web-dev-qa-db-ja.com

Java Interface in Java 1.6を使用せずに「プログラムをインターフェイスに」強制する方法

Java 1.8では、新しい「デフォルトのインターフェースメソッド」が追加されました。 1.6では、どれくらい近づくことができますか?目標:コードを使用して、クラスがJavaインターフェースではないことをクライアントが認識できないようにします。 クラス名をJavaインターフェイスにリファクタリングする権利を保持するの場合、ここでJava interfaceを作成する必要はありません。 classを提供するだけです。後で、クラス名がインターフェイスになるようにリファクタリングしても、クライアントコードに触れることはありません。

のTelastynの回答から「インターフェイスへのプログラミング」について理解する

  • 「依存関係の逆転の原則」は、オブジェクトは依存関係の作成を制御するべきではなく、必要な依存関係を通知し、呼び出し側がそれを提供できるようにする必要があると述べています。ただし、依存関係が具象型であるかインターフェイスであるかは指定されていません。

この区別のなさを活かしたい。また、「インターフェイスへのプログラム」という原則を解釈して、「クライアントコードがJavaインターフェイスまたはJavaクラスで動作しているかどうかを判別できない」と解釈したいと思います。そのため、後で誰かがクラスを同じ名前のJavaインターフェースに交換でき、クライアントコードを変更する必要がなくなります。これは非常に厳格に見えるかもしれませんが、これらの設計原則(Javaの特定の癖を無視しようとする)が意味するものではないかもしれませんが、私はそれを取り除きたいと思います。

理由は次のとおりです。私たちの開発環境は継続的な統合を使用していません。誰もがコードを統合することを強制するリリースは、1回おきに発生します...まあ、私がそこで働いていたすべての年に起こったわけではありません。さらに悪いことに、私が正当化しようとしない理由のために、テストは非常に手作業です。つまり、他の人が使用するクラスを書くことは、本質的に公開されます。これを変更すると、誰かのストリームが壊れたり、クローズされてハードにクローズされたテスト済みコードが壊れたりするからです。これは、共有クラスでパブリックの署名をリファクタリングしないことを意味します。ライブラリのAPIを書くのと同じように感じます。

私はYAGNIを信じています。 BDUFをやりたくない。しかし、今のところ、実装は1つしかありませんが、この状況では、すべての共有クラスにJavaインターフェイス(big I)を配置するなど、ばかげたことをするように誘惑されます。これはYAGNIに反するものであり、不要なインターフェースを混乱させると考える開発者を困らせます。

私が本当に望んでいるのは、そのインターフェースを自由に追加できること、そして必要に応じて後で別の実装を追加することです。私が広く共有しているクラスに対して書き込まれる可能性のあるすべてのコードを見ることができない場合でも、これを確実に実行できるようにしたいと思います。私がコンパイラーを使用してユーザーをインターフェースにプログラミングするように強制した場合にのみ、確信が持てます(小さなi)。

質問:信頼できるパッケージの外部からのリフレクションを使用して、この保護を解除できますか?これを達成するための次の方法は正しいですか?この環境では、これを正しく行うために1つのショットしか得られません。とにかく、信頼できるパッケージの外部から古い実装にロックダウンする必要があるかどうかを知りたいです。コメント付きのコードはすべて「将来の」コードです。常にエラーであるはずのメインのエラーを除いて。

ここでは、誰かが私たちを実装に制限するために最善を尽くしているのを見ます:

package untrusted;

import trusted.AmIAnInterfaceWithDefaultImplementation;
import trusted.WhoKnowsWhatImplementation;

public class Activate {

    public static void main(String[] args) {
        System.out.println( WhoKnowsWhatImplementation.thisProvides().letsFindOut() );

        AmIAnInterfaceWithDefaultImplementation i =                             
            WhoKnowsWhatImplementation.thisProvides();

        System.out.println( i.letsFindOut() );

        //Error: AmIAnInterfaceWithDefaultImplementation i = new AmIAnInterfaceWithDefaultImplementation();
        //Error: The constructor AmIAnInterfaceWithDefaultImplementation() is not visible
    }
    //Error: public class ImGonnaForceYouToBeAClass extends AmIAnInterfaceWithDefaultImplementation {}
    //Error: The type ImGonnaForceYouToBeAClass cannot subclass the final class AmIAnInterfaceWithDefaultImplementation
}

印刷物:Default implementationを2回。

ここでは、静的ファクトリメソッドで提供する実装を選択します。それは、コンストラクターが機能するのが同じパッケージ内にあるためだけです。

package trusted;

public class WhoKnowsWhatImplementation {
     public static AmIAnInterfaceWithDefaultImplementation thisProvides() {
        return new AmIAnInterfaceWithDefaultImplementation();
        //return new IAintNoInterfaceNoMore();
    }
}

以下のクラスはインターフェース(小さいi)と実装の両方を提供しますが、唯一のコンストラクターが保護されており、クラスが最終的なものであるため、信頼できるパッケージの外部にあるインターフェースと実装を強制的に連携させることはできません。

package trusted;

/** 
 *  Please don't instantiate directly as this may become an Interface.
 *  Use WhoKnowsWhatImplementation 
 */ 
public final class AmIAnInterfaceWithDefaultImplementation {

    protected AmIAnInterfaceWithDefaultImplementation(){}

    public String letsFindOut() {
        return "Default implementation";
    }
}

//public interface AmIAnInterfaceWithDefaultImplementation{
//  public String letsFindOut();
//}

インターフェースに自由に切り替えて、実装を新しいクラスにプッシュできます。

package trusted;

//public class IAintNoInterfaceNoMore implements AmIAnInterfaceWithDefaultImplementation {
//
//  @Override
//  public String letsFindOut() {
//      return "Shiny new implementation";
//  }
//}

「信頼された」パッケージの必要性は、インスタンス化しているのと同じクラスで静的ファクトリーメソッドを有効にしたくないという欲求に由来します。クライアントコードに影響を与えるJavaインターフェイスに切り替えたときに、それは移動する必要があります。そのため、コンストラクタはパッケージにアクセスできる必要がありました。

リフレクションを使用せずに、信頼済みパッケージの外部からクラスとしてこれをロックできますか?ロックする方法は、保護するのに妥当なものですか?ロックダウンから保護するために他に何をしますか? Java 1.6でより良いアプローチはありますか?

また、Javaドキュメントを超えてリフレクションから保護する方法があるかどうかも知りたいです。直接インスタンス化しないようにお願いしていますが、ないのではないかと思います。

多くのデザインは、すべてのデザイン原則に準拠していません。これはオープンクローズの原則に違反しています。クラスは拡張に対してオープンであるが、変更に対してクローズでなければなりません。ただし、これは、Javaインターフェースに変更された場合でも、クラスクライアントを閉じたままにできるようにするための試みです。

ここに示すリファクタリングの例は、interfaceと実装classに切り替えます。代わりに、クラスが実際にabstractであると決定される可能性があることに注意してください。 Java 1.8に切り替えると、デフォルトのメソッドを持つインターフェースとなる可能性もあります。重要なのは、後でこの決定を行う権利を保持することです。これにより、何をすべきかわからず、本当に気にしないときに、今すぐ決定する必要はありません。

編集:私はこれらすべての答えに感謝します。私がそれを受け入れる前に、私が待っているのは、このアプローチがクラスであることに縛られることに対する脆弱性の分析です。私はそれを永遠にクラスとする運命にあるコードを見せることによって、この考え全体を打ちのめすものを受け入れたいと思っています。それが健全なアイデアだと主張するものを受け入れることは素晴らしいエゴブーストですが、私はコードベースよりもここで本当に失敗したいと思います。それが健全なアイデアだと私に納得させるのは難しい仕事です。

これらの回答から私が学んだこと:

  • クラスからJavaインターフェースに変換するには、クライアントを再コンパイルする必要があります
  • クラスは、Javaインターフェースが公開できる以上のものを公開してはなりません(たとえば、静的メソッドがない)。
  • 一部のクラスは、必要なときに関係なく、Javaインターフェイスを事前に実装するよう要求します sigh

クライアントコードを再コンパイルする必要があることは、認識すべき最も重要な問題であると私は思います。そのため、Maarten Winkelsの回答を受け入れます。コーディングありがとうございました。

7
candied_orange

コードで「クラスへのインターフェイスをロックダウン」する方法はわかりませんが、次の2つを考慮する必要があると思います。

  1. クライアントのソースコードを変更する必要がない場合でも、クライアントはコードを再コンパイルする必要があります。インターフェイスでメソッドを呼び出すと、インスタンスでメソッドを呼び出す場合とは異なるバイトコード操作が使用されます。
  2. 同僚が信頼できるパッケージでクラスを作成し、そこでコンストラクターを呼び出すのを妨げるものは何ですか?
2
Maarten Winkels

まず、リテラルJavaインターフェイスへのプログラミングではなく、コントラクトへのプログラミングという意味で、インターフェイスへのプログラムを取り上げます。コントラクトは、クラスまたはインターフェース(または関数)に実装できます。

この区別のなさを活かしたい。また、「インターフェイスへのプログラム」という原則を解釈して、「(コードを使用して)クライアントコードがJavaインターフェイスまたはJavaで動作しているかどうかを判断できないようにする必要があります。クラス"。そのため、後で誰かがクラスを同じ名前のJavaインターフェースに交換でき、クライアントコードを変更する必要がなくなります。これは非常に厳格に見えるかもしれませんが、これらの設計原則(Javaの特定の癖を無視しようとする)が意味するものではないかもしれませんが、私はそれを取り除きたいと思います。

私の職場でも同様の問題が発生しています。すべてにインターフェースがある場合、複数の実装があるものとないものを見失う。コードを書くとき、いくつかの実装があることに気にしないかもしれません。しかし、コードを読んだり、コードをデバッグしたりするときに気になるかもしれません。

最初にクラスを使用し、インターフェースにリファクタリングすることには、いくつかの潜在的な問題があります。

私が見ることができる問題の1つは、公開している、またはパッケージ(デフォルト)アクセスレベルメソッドが多すぎることです。クライアントが、インターフェイスの一部ではないpublicまたはpackage protectedメソッドを使用できる場合、クラスの代わりにインターフェイスを安全に交換できるとは保証できません。

理論的には、クラスは、インターフェイスに存在するメソッドを外部にのみ公開する場合、インターフェイスの目的を果たします。あなたがクラスに厳格であり、インターフェイスにあるメソッドのみを公開することができるなら、私はあなたがそれらをどのように入れ替えることができなかったかを見ることができません。

別の問題は、クラスの直接構築を使用するクライアントです。しかし、インターフェースは私がこれをするのを妨げません:

 Foo bar = new DefaultFoo();

インスタンスを実際にスワップインおよびスワップアウトできるようにするには、何らかのフレーバーの依存性注入が必要になります。また、複数の実装を作成する場合でも、クライアントコードを変更する必要がある場合があります。このクライアントにFancyFooとPlainFooのどちらが必要かを知っておく必要があります。

最後の懸念は、クラスが実際に複数のインターフェースを実装しているかどうかです。 FooインターフェースとBarインターフェースの両方を実装するFrobnicatorがある場合、インターフェースをクラスに交換するのは非常に困難です。 Frobnicatorを使用するクライアントは、Fooのみが必要なときにBar機能を呼び出している可能性があり、その逆も同様です。

2
Peter Smith

インターフェースを作成することが問題の最良の解決策であり、それらに対する唯一の議論は次のとおりです。

  • 複数の実装はありません(YAGNI)

    私が本当に望んでいるのは、必要に応じて、そのインターフェースと代替実装を後で追加する自由です。

    インターフェースは、実行時に実装を切り替えるだけのものではありません。クライアントコードを変更せずにAPIの背後で実装を変更できるようにすることは、capital-Iインターフェースを定義する正当な理由です。

  • 仲間の開発者は実装にナビゲートするのが難しいと感じています

    (...)コードをクリックしてインターフェイスではなく実装に直接アクセスできるようにしたい開発者を困らせます

    Eclipseで特定のメソッド呼び出しの実装に移動するのは簡単です。Ctrlキーを押しながらメソッド呼び出しにカーソルを合わせます。 Open Implementationオプションを含むドロップダウンが表示されます。

    IntelliJはしばらく使用していませんが、使用できます 同様に

簡単にインターフェースをあきらめないでください。あなたの問題を解決することは彼らの主な利点の一つです。

2
Mike Partridge

正しい解決策は、インターフェースと単純なファクトリーの組み合わせを使用することです。これはあなたが行っている一般的な方向であり、正しい方法です。これをより簡単にするために、果物の類推を使用してみましょう。 (名前はかなり短くなっています。)

Fruitインターフェースはtrustedパッケージにあります:

package trusted;

public interface Fruit
{
    public void nomnom();
}

FruitAppleの単一の実装があります。エンドユーザーはApplesを使用できるが、Appleを直接インスタンス化できないようにしたい:

package trusted;

final class Apple implements Fruit
{
    @Override
    public void nomnom() {
        System.out.println(nom3() + ", Apple!");
    }

    /* Not in Fruit */
    public String nom3() {
        return "Nom, nom, nom";
    }
}

Appletrustedパッケージに含まれています。まず、Appleには可視性修飾子(パブリック、プライベートなど)がないことに注意してください。これは、Appleにデフォルトの可視性があることを意味します。これはたまたま、package-privateに似ています。つまり、trustedのクラスだけがAppleに関するすべてにアクセスできます。 trustedの外では、Appleはインスタンス化できず(たとえパブリックコンストラクターを提供している場合でも!)、静的メソッドを呼び出すことはできません。 Appleについては誰も知ることができません。これにより、クライアントがAppleを必要とする場合にFruitを取得できるようにするという小さな問題が発生します。直接作成することはできないため、FruitFactoryパッケージ内でtrustedにパブリックな可視性を提供する必要があります。

package trusted;

public class FruitFactory
{
    /* This could also be an instance method */
    public static Fruit getFruit() {
        return new Apple();
    }
}

これで、Fruitの実装を公開せずにApple(この場合はApple)を取得する方法ができました。実際の動作を確認できます。

package untrusted;

import trusted.*;

public class Main
{
    public static void main(String[] args) {
        Fruit f = FruitFactory.getFruit();
        f.nomnom();
    }
}

そして、すべてがうまくいきます!面倒なくFruitを取得できます。 Appleの外部からtrustedをインスタンス化または直接アクセスしようとすると、コンパイラエラーが発生します。

ここで、最新かつ最高のフルーツテクノロジーOrangeを思いついたとしましょう。 Appleは古い帽子なので、代わりに工場からOrangeを全員に提供します。まず、Orange

package trusted;

final class Orange implements Fruit
{
    @Override
    public void nomnom() {
        System.out.println("Nom nom nom, orange!");
    }
}

そして今、全員をOrangeバンドワゴンに乗せるために加えなければならないすべての変更:

package trusted;

public class FruitFactory
{
    public static Fruit getFruit() {
        return new Orange();
    }
}

できました。 trusted以外は何も変更する必要がないことに注意してください。 Mainには触れませんでした。 Appleにも触れませんでした。 Orangeを追加し、ファクトリークラスを変更しました(オープン/クローズではない唯一の変更ですが、高度にローカライズされ分離されています)。 Fruit実装ではなくAppleインターフェースにプログラムを設定しているため、すべて正常にコンパイルされます。彼らが知る必要があるのは、FruitFactoryを使用してFruitを取得することだけであり、残りの部分(実装の詳細)について心配する必要はありません。

インターフェースにプログラミングしているので、実装がどのように見えるかを知る必要がないことに注意してください。 Fruitインターフェースだけを見て、AppleOrangeを見ないことで、クライアントを完全に構築できるはずです。


あなたは間違いなくインターフェースを使用すべきであり、クラスとしてインターフェースを「偽造」しようとせず、後でそれを変更しようとしないと述べました。これにはいくつかの理由があります。

  1. クラスから始めると、APIが実装に依存するような方法でクラスを作成してしまう可能性があります。インターフェース(big-I)を書くことで、目の前に何も実装しなくても、何かをするつもりでhowについて考える必要がなくなります。代わりに、やりたいwhatに焦点を当てます。
  2. インターフェイスを作成すると、必要最小限の情報を公開できます。インターフェイスには実装が含まれていないため、それをチームの他のメンバーに公開すると、公開されているAPIだけが認識されます。実装されたクラスを公開すると、意図したよりも大きなAPIが含まれる場合があります(注nom3 in Apple、これは非表示にしておく必要があります)。他の人がこれらのエクストラに依存し始めると、実装の詳細を意図しただけであっても、変更することはできません。公開された実装には、APIに固有ではない可能性がある特定の特性(パフォーマンスなど)がある場合もあります。他のものはその実装のために最適化するかもしれませんが、何か新しいことを思いついたときに問題が発生するだけです。
  3. 必要最小限の量を公開することで、あなたは自由を得ます。あなたは常に自由を必要としています。プライベートAPIを使用すると、変更を加えるだけの自由がありますが、すべてのAPIは基本的に公開されるため、できるだけ多くの自由を確保する必要があります。 コンパイル時に実装を交換する必要がありますApplesおよびOrangesのように)?問題ない。 runtimeで実装を切り替える必要がありますか?問題ありません。工場に必要なものを理解させてください。デコレーターやアダプターなど、別のパターンを使用する必要がある機能を実装する必要がありますか?問題ない。必要な実装の周りにインターフェースを実装するだけです。クラスからインターフェースへのリファクタリングを後で行う必要はありません。つまり、よりオープン/クローズすることもできます。

他の人が技術的に予期しない方法でコンポーネントを使用できないようにすることを非常に心配しているようです。これはそれを達成しますが、決心した人はあなたがしようとすることを何でも回避する方法を常に見つけることを忘れないでください。簡単な道筋を文書化し、それが最高の理由を示します。ほとんどの人はあなたが提供する最も簡単な道を進み、しっかりと眠ることができます。

2
cbojar