私が読んだほとんどすべての Java 本では、オブジェクト間で状態と動作を共有する方法としてインターフェイスを使用することについて説明しています。
しかし、アーキテクトがアプリケーションを設計するのを見ると、彼らが最初に行うことは、インターフェイスのプログラミングを開始することです。どうして?そのインターフェース内で発生するオブジェクト間のすべての関係をどのように知ることができますか? これらの関係が既にわかっている場合は、抽象クラスを拡張してみませんか?
私が読んだほとんどすべての Java 本では、オブジェクト間で状態と動作を共有する方法としてインターフェイスを使用することについて説明しています。
しかし、アーキテクトがアプリケーションを設計するのを見ると、彼らが最初に行うことは、インターフェイスのプログラミングを開始することです。どうして?そのインターフェース内で発生するオブジェクト間のすべての関係をどのように知ることができますか? これらの関係が既にわかっている場合は、抽象クラスを拡張してみませんか?
インターフェースへのプログラミングとは、そのインターフェースを使用して作成された「契約」を尊重することを意味します。したがって、IPoweredByMotor
インターフェイスにstart()
メソッドがある場合、そのインターフェイスのメソッドを実装する際に、インターフェイスを実装する将来のクラスはMotorizedWheelChair
、 、Automobile
、またはSmoothieMaker
であり、システムに柔軟性を追加します。 1 つのコードが知る必要があるのは、それらが に応答することだけだからstart()
です。どのように開始するかは問題ではありません。開始する必要があるだけです。
素晴らしい質問です。抽象クラスよりもインターフェイスの使用を好む理由 (項目 16) を書いているJosh Bloch を紹介します。ちなみに、この本を持っていない方は是非オススメです!彼の言うことの要約は次のとおりです。
基本的な実装を提供する抽象クラスの利点はどうですか? 各インターフェースに抽象骨格実装クラスを提供できます。これにより、インターフェイスと抽象クラスの両方の長所が組み合わされます。スケルトン実装は、抽象クラスが型定義として機能するときに強制する厳しい制約を課すことなく、実装支援を提供します。たとえば、コレクション フレームワークは、インターフェイスを使用して型を定義し、それぞれに骨組みの実装を提供します。
インターフェイスへのプログラミングには、いくつかの利点があります。
訪問者パターンなどの GoF タイプのパターンに必要
代替実装を可能にします。たとえば、使用中のデータベース エンジンを抽象化する単一のインターフェイスに対して、複数のデータ アクセス オブジェクトの実装が存在する場合があります (AccountDaoMySQL と AccountDaoOracle の両方が AccountDao を実装する場合があります)。
クラスは複数のインターフェースを実装できます。Java では、具象クラスの多重継承は許可されていません。
要約実装の詳細。インターフェイスには、実装の詳細を隠して公開 API メソッドのみを含めることができます。利点には、明確に文書化されたパブリック API と十分に文書化されたコントラクトが含まれます。
http://www.springframework.org/などの最新の依存性注入フレームワークで頻繁に使用されます。
Java では、インターフェイスを使用して動的プロキシを作成できます - http://java.sun.com/j2se/1.5.0/docs/api/java/lang/reflect/Proxy.html。これは、アスペクト指向プログラミングを実行するために、Spring などのフレームワークで非常に効果的に使用できます。アスペクトは、クラスに Java コードを直接追加しなくても、非常に便利な機能をクラスに追加できます。この機能の例には、ロギング、監査、パフォーマンス監視、トランザクション境界などがあり ます。 http://static.springframework.org/spring/docs/2.5.x/reference/aop.html。
モック実装、単体テスト - 依存クラスがインターフェースの実装である場合、それらのインターフェースも実装するモック クラスを作成できます。モック クラスを使用して、単体テストを容易にすることができます。
抽象クラスが開発者によってほとんど放棄された理由の 1 つは、誤解にあるのではないかと思います。
ギャング・オブ・フォーが書いたとき:
実装ではなくインターフェイスにプログラムします。
Java や C# インターフェイスなどはありませんでした。彼らは、すべてのクラスが持っているオブジェクト指向インターフェースの概念について話していました。Erich Gamma がこのインタビューで言及しています。
すべてのルールと原則を考えずに機械的に従うと、コードベースの読み取り、ナビゲート、理解、保守が困難になると思います。覚えておいてください:おそらくうまくいく最も簡単なこと。
どうして?
それはすべての本が言っていることだからです。GoF パターンと同様に、多くの人はそれを普遍的に優れたものと見なし、それが本当に正しい設計であるかどうかについて考えたことはありません。
そのインターフェース内で発生するオブジェクト間のすべての関係をどのように知ることができますか?
あなたはしません、そしてそれは問題です。
これらの関係が既にわかっている場合は、抽象クラスを拡張してみませんか?
抽象クラスを拡張しない理由:
どちらにも当てはまらない場合は、抽象クラスを使用してください。時間を大幅に節約できます。
聞かなかった質問:
インターフェイスを使用することのマイナス面は何ですか?
それらを変更することはできません。抽象クラスとは異なり、インターフェイスは固定されています。一度使用すると、それを拡張するとコードが壊れます。
本当に必要ですか?
ほとんどの場合、いいえ。オブジェクト階層を構築する前に、よく考えてください。Java のような言語の大きな問題は、大規模で複雑なオブジェクト階層を簡単に作成できることです。
LameDuck が Duck から継承する典型的な例を考えてみましょう。簡単ですね。
まあ、それは、アヒルが怪我をして足が不自由になったことを示す必要があるまでです。または、足の不自由なアヒルが癒され、再び歩けるようになったことを示します。Java ではオブジェクト タイプを変更できないため、サブタイプを使用してラメネスを示すことは実際には機能しません。
インターフェイスへのプログラミングとは、そのインターフェイスを使用して作成された「契約」を尊重することを意味します
これは、インターフェースに関して最も誤解されている唯一のことです。
インターフェイスとのそのような契約を強制する方法はありません。インターフェースは、定義上、動作をまったく指定できません。クラスは、動作が発生する場所です。
この誤った信念は非常に広まっているため、多くの人が従来の知識と見なしています。しかし、それは間違っています。
したがって、OPのこのステートメント
私が読んだほとんどすべてのJavaの本は、オブジェクト間で状態と動作を共有する方法としてインターフェースを使用することについて説明しています。
不可能です。インターフェイスには状態も動作もありません。彼らは、実装クラスが提供しなければならないプロパティを定義できますが、それは彼らが得ることができる限り近いものです。インターフェイスを使用して動作を共有することはできません。
あなたは人々がそのメソッドの名前によって暗示されるような振る舞いを提供するためにインターフェースを実装するだろうと仮定することができますが、それは同じことのようなものではありません。また、そのようなメソッドが呼び出されるタイミングにはまったく制限がありません(たとえば、Stopの前にStartを呼び出す必要があります)。
この文
ビジターパターンなどのGoFタイプのパターンに必要
また、正しくありません。GoFの本は、当時使用されていた言語の機能ではなかったため、まったくゼロのインターフェースを使用しています。一部のパターンはインターフェースを使用できますが、どのパターンもインターフェースを必要としません。IMO、オブザーバーパターンは、インターフェイスがより洗練された役割を果たすことができるパターンです(ただし、このパターンは現在、通常、イベントを使用して実装されています)。ビジターパターンでは、ほとんどの場合、訪問先ノードのタイプごとにデフォルトの動作を実装する基本ビジタークラスであるIMEが必要です。
個人的には、この質問に対する答えは3つあると思います。
インターフェイスは多くの人に特効薬と見なされています(これらの人々は通常、「契約」の誤解の下で働いているか、インターフェイスが魔法のようにコードを切り離していると考えています)
Javaの人々はフレームワークの使用に非常に焦点を合わせており、その多くは(当然のことながら)インターフェースを実装するためにクラスを必要とします
ジェネリックスとアノテーション(C#の属性)が導入される前は、インターフェイスがいくつかのことを行うための最良の方法でした。
インターフェイスは非常に便利な言語機能ですが、悪用されています。症状は次のとおりです。
インターフェイスは1つのクラスによってのみ実装されます
クラスは複数のインターフェースを実装します。多くの場合、インターフェースの利点として宣伝されていますが、これは通常、問題のクラスが関心の分離の原則に違反していることを意味します。
インターフェイスの継承階層があります(多くの場合、クラスの階層によってミラーリングされます)。これは、そもそもインターフェイスを使用して回避しようとしている状況です。継承が多すぎることは、クラスとインターフェースの両方にとって悪いことです。
これらはすべてコードの臭いです、IMO。
私の意見では、これは非常に優れた方法であり、間違った状況で適用されることが多いためです。
抽象クラスと比較して、インターフェイスには多くの利点があります。
コードのモジュールを扱う場合、インターフェースから最大の利点が得られます。ただし、モジュールの境界をどこに置くべきかを判断する簡単なルールはありません。したがって、このベスト プラクティスは、特に最初にソフトウェアを設計する場合に、使いすぎてしまいがちです。
これは、疎結合を促進する 1 つの方法です。
低結合では、1 つのモジュールを変更しても、別のモジュールの実装を変更する必要はありません。
この概念の良い使い方は、Abstract Factory パターンです。ウィキペディアの例では、GUIFactory インターフェイスが Button インターフェイスを生成します。具体的なファクトリは、WinFactory (WinButton を生成する) または OSXFactory (OSXButton を生成する) の場合があります。OldButton
GUI アプリケーションを作成していて、クラスのすべてのインスタンスを調べて、それらを に変更する必要があると想像してくださいWinButton
。そして来年、OSXButton
バージョンを追加する必要があります。
私は(@ eed3s9nで)それが疎結合を促進すると仮定します。また、インターフェイスがないと、オブジェクトをモックアップできないため、単体テストがはるかに難しくなります。
なぜ拡張するのは悪です。この記事は、尋ねられた質問に対するほとんどの直接的な回答です。実際に抽象クラスが必要になるケースはほとんどないと思いますが、それが悪い考えである状況はたくさんあります。これは、抽象クラスを使用した実装が悪いという意味ではありませんが、インターフェイス コントラクトが特定の実装のアーティファクトに依存しないように注意する必要があります (適切な例: Java の Stack クラス)。
もう 1 つ: インターフェイスをあらゆる場所に配置する必要はありません。通常、インターフェイスが必要な場合と不要な場合を特定する必要があります。理想的な世界では、ほとんどの場合、2 番目のケースは final クラスとして実装する必要があります。
ここにはいくつかの優れた回答がありますが、具体的な理由を探している場合は、単体テストを参照してください。
ビジネス ロジックで、トランザクションが発生する地域の現在の税率を取得するメソッドをテストするとします。これを行うには、ビジネス ロジック クラスがリポジトリを介してデータベースと通信する必要があります。
interface IRepository<T> { T Get(string key); }
class TaxRateRepository : IRepository<TaxRate> {
protected internal TaxRateRepository() {}
public TaxRate Get(string key) {
// retrieve an TaxRate (obj) from database
return obj; }
}
コード全体で、TaxRateRepository の代わりに IRepository タイプを使用します。
リポジトリには、ユーザー (開発者) がファクトリを使用してリポジトリをインスタンス化することを奨励する非パブリック コンストラクターがあります。
public static class RepositoryFactory {
public RepositoryFactory() {
TaxRateRepository = new TaxRateRepository(); }
public static IRepository TaxRateRepository { get; protected set; }
public static void SetTaxRateRepository(IRepository rep) {
TaxRateRepository = rep; }
}
ファクトリは、TaxRateRepository クラスが直接参照される唯一の場所です。
したがって、この例にはいくつかのサポート クラスが必要です。
class TaxRate {
public string Region { get; protected set; }
decimal Rate { get; protected set; }
}
static class Business {
static decimal GetRate(string region) {
var taxRate = RepositoryFactory.TaxRateRepository.Get(region);
return taxRate.Rate; }
}
また、別の IRepository の実装 (モックアップ) もあります。
class MockTaxRateRepository : IRepository<TaxRate> {
public TaxRate ReturnValue { get; set; }
public bool GetWasCalled { get; protected set; }
public string KeyParamValue { get; protected set; }
public TaxRate Get(string key) {
GetWasCalled = true;
KeyParamValue = key;
return ReturnValue; }
}
ライブ コード (ビジネス クラス) は Factory を使用してリポジトリを取得するため、単体テストでは TaxRateRepository の MockRepository をプラグインします。置換が行われると、戻り値をハードコーディングして、データベースを不要にすることができます。
class MyUnitTestFixture {
var rep = new MockTaxRateRepository();
[FixtureSetup]
void ConfigureFixture() {
RepositoryFactory.SetTaxRateRepository(rep); }
[Test]
void Test() {
var region = "NY.NY.Manhattan";
var rate = 8.5m;
rep.ReturnValue = new TaxRate { Rate = rate };
var r = Business.GetRate(region);
Assert.IsNotNull(r);
Assert.IsTrue(rep.GetWasCalled);
Assert.AreEqual(region, rep.KeyParamValue);
Assert.AreEqual(r.Rate, rate); }
}
リポジトリ、データベース、接続文字列などではなく、ビジネス ロジック メソッドのみをテストすることを忘れないでください。それぞれに異なるテストがあります。このようにすることで、テストしているコードを完全に分離できます。
副次的な利点は、データベースに接続せずに単体テストを実行できることです。これにより、より高速で移植性が高くなります (複数の開発者チームが遠隔地にいることを考えてください)。
もう 1 つの副次的な利点は、開発の実装フェーズでテスト駆動開発 (TDD) プロセスを使用できることです。私は厳密には TDD を使用していませんが、TDD と昔ながらのコーディングを組み合わせて使用しています。
Javaでインターフェースを使用する主な理由は、単一継承への制限だと思います。多くの場合、これは不必要な複雑化とコードの重複につながります。Scalaのトレイトを見てください:http: //www.scala-lang.org/node/126トレイトは特別な種類の抽象クラスですが、クラスはそれらの多くを拡張できます。
コーディング前の設計がすべてです。
インターフェイスを指定した後で 2 つのオブジェクト間のすべての関係がわからない場合は、インターフェイスの定義が不十分であり、比較的簡単に修正できます。
コーディングに直接飛び込んで、途中で何かが欠けていることに気付いた場合、修正するのははるかに困難です。
理由の 1 つは、インターフェースが成長と拡張性を可能にすることです。たとえば、オブジェクトをパラメーターとして受け取るメソッドがあるとします。
public void drink(コーヒー someDrink) {
}
ここで、まったく同じメソッドを使用したいが、hotTea オブジェクトを渡したいとしましょう。まあ、あなたはできません。コーヒー オブジェクトのみを使用するように、上記のメソッドをハードコーディングしました。それがいいのかもしれないし、悪いのかもしれない。上記の欠点は、あらゆる種類の関連オブジェクトを渡したいときに、1 つのタイプのオブジェクトに厳密にロックインされることです。
IHotDrink などのインターフェイスを使用して、
インターフェイス IHotDrink { }
オブジェクトの代わりにインターフェイスを使用するように上記のメソッドを書き直して、
public void drink(IHotDrink someDrink) {
}
これで、IHotDrink インターフェイスを実装するすべてのオブジェクトを渡すことができます。確かに、まったく同じことを別のオブジェクト パラメータで行うまったく同じメソッドを作成できますが、なぜでしょうか? あなたは突然肥大化したコードを維持しています。
これは perl/python/ruby の観点から見ることができます:
これを最もよく説明するのは、Java インターフェイスをそれに類推することだと思います。実際には type を渡すのではなく、メソッドに応答するもの (必要に応じて trait ) を渡すだけです。
ある意味では、あなたの質問は単純に「なぜ抽象クラスではなくインターフェイスを使用するのですか?」に帰着すると思います。技術的には、両方の疎結合を実現できます。基になる実装はまだ呼び出し元のコードに公開されていません。また、Abstract Factory パターンを使用して基になる実装 (インターフェイス実装と抽象クラス拡張) を返すことで、実装の柔軟性を高めることができます。デザイン。実際、抽象クラスは、コードを満たすために実装を要求する (「start() を実装しなければならない」) ことと、デフォルトの実装を提供する (「私は標準的な paint() を持っています必要に応じてオーバーライドできます") -- インターフェイスでは、実装を提供する必要がありますが、時間の経過とともに、インターフェイスの変更によって脆弱な継承の問題が発生する可能性があります。
ただし、基本的には、主に Java の単一継承の制限により、インターフェイスを使用します。私の実装が、コードを呼び出すことによって使用される抽象クラスから継承しなければならない場合、それは他の何かから継承する柔軟性を失うことを意味します (コードの再利用やオブジェクト階層など)。