なぜほとんどのシステムアーキテクトは最初にインターフェースに対してプログラミングすることにこだわるのでしょうか?
-
09-06-2019 - |
質問
私が読んだほぼすべての Java 本では、最初に「構築」されたときには関係を共有していないようだったオブジェクト間で状態と動作を共有する方法としてインターフェイスを使用することについて説明しています。
しかし、アーキテクトがアプリケーションを設計しているのを見ると、最初にインターフェイスへのプログラミングを開始します。どうして?そのインターフェイス内で発生するオブジェクト間のすべての関係をどのようにして知ることができるのでしょうか?これらの関係をすでに知っている場合は、抽象クラスを拡張するだけではどうでしょうか?
解決
インターフェイスにプログラミングするということは、そのインターフェイスを使用して作成された「契約」を尊重することを意味します。そして、もしあなたの場合、 IPoweredByMotor
インターフェースには start()
メソッド、インターフェイスを実装する将来のクラス、 MotorizedWheelChair
, Automobile
, 、 または SmoothieMaker
, 、そのインターフェイスのメソッドを実装する際に、システムに柔軟性を加えます。1 つのコードでさまざまな種類のモーターを起動できるためです。1 つのコードで知っておく必要があるのは、それらが応答することだけであるためです。 start()
. 。それは問題ではありません どうやって 彼らは始めます、ただ彼らは 始めなければなりません.
他のヒント
素晴らしい質問です。参考にさせていただきます Josh Bloch の「Effective Java」, 、抽象クラスよりもインターフェイスの使用を好む理由 (項目 16) を書いた人。ちなみに、この本をまだお持ちでない方には、ぜひお勧めします!彼の言っていることの要約は次のとおりです。
- 既存のクラスを簡単に改造して、新しいインターフェイスを実装できます。 必要なのは、インターフェイスを実装し、必要なメソッドを追加することだけです。既存のクラスを簡単に改造して新しい抽象クラスを拡張することはできません。
- インターフェイスはミックスインを定義するのに最適です。 ミックスイン インターフェイスを使用すると、クラスで追加のオプションの動作 (Comparable など) を宣言できます。これにより、オプションの機能を主要な機能と組み合わせることができます。抽象クラスはミックスインを定義できません。クラスは複数の親を拡張できません。
- インターフェイスにより、非階層フレームワークが可能になります。 多くのインターフェイスの機能を持つクラスがある場合、そのクラスはそれらをすべて実装できます。インターフェイスがなければ、属性の組み合わせごとにクラスを含む肥大化したクラス階層を作成する必要があり、その結果、組み合わせが爆発的に増加します。
- インターフェイスにより、安全な機能強化が可能になります。 堅牢かつ柔軟な設計である Decorator パターンを使用してラッパー クラスを作成できます。ラッパー クラスは同じインターフェイスを実装して含み、一部の機能を既存のメソッドに転送すると同時に、特殊な動作を他のメソッドに追加します。抽象メソッドではこれを行うことはできません。代わりに、より脆弱な継承を使用する必要があります。
基本的な実装を提供する抽象クラスの利点についてはどうでしょうか?各インターフェイスに抽象骨格実装クラスを提供できます。これにより、インターフェイスと抽象クラスの両方の長所が組み合わされます。スケルタル実装は、抽象クラスが型定義として機能するときに強制する厳しい制約を課すことなく、実装支援を提供します。たとえば、 コレクションフレームワーク インターフェイスを使用して型を定義し、それぞれの骨格となる実装を提供します。
インターフェイスへのプログラミングには、いくつかの利点があります。
訪問者パターンなどの 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.
モック実装、単体テスト - 依存クラスがインターフェイスの実装である場合、それらのインターフェイスも実装するモック クラスを作成できます。モック クラスを使用すると、単体テストを容易にすることができます。
どうして?
だって、どの本にもそう書いてあるから。GoF パターンと同様に、多くの人はそれが普遍的に優れていると考えており、それが本当に正しいデザインであるかどうかについて考えたことはありません。
そのインターフェイス内で発生するオブジェクト間のすべての関係をどのようにして知ることができるのでしょうか?
そうしません、それは問題です。
あなたがすでにそれらの関係を知っているなら、なぜ抽象クラスを拡張しないのですか?
抽象クラスを拡張しない理由:
- 根本的に異なる実装があり、適切な基本クラスを作成するのは非常に困難です。
- 唯一の基本クラスを別のもののために書き込む必要があります。
どちらにも当てはまらない場合は、抽象クラスを使用してください。時間を大幅に節約できます。
あなたが尋ねなかった質問:
インターフェイスを使用することの欠点は何ですか?
変更することはできません。抽象クラスとは異なり、インターフェイスは固定されています。一度使用すると、それを拡張するとコードが壊れてしまいます。
本当にどちらも必要ですか?
ほとんどの場合、そうではありません。オブジェクト階層を構築する前に、よく考えてください。Java のような言語の大きな問題は、大規模で複雑なオブジェクト階層を簡単に作成してしまうことです。
LameDuck が Duck から継承した典型的な例を考えてみましょう。簡単そうに思えますよね?
さて、それは、アヒルが怪我をして足が不自由になったことを示す必要があるまでのことです。あるいは、レームアヒルが治癒して再び歩けるようになったことを示します。Java ではオブジェクトのタイプを変更できないため、サブタイプを使用して不自由さを示すことは実際には機能しません。
インターフェイスへのプログラミングとは、そのインターフェイスを使用して作成された「契約」を尊重することを意味します
これはインターフェイスに関して最も誤解されている点です。
インターフェイスに対してそのような契約を強制する方法はありません。定義上、インターフェイスでは動作をまったく指定できません。クラスは行動が起こる場所です。
この誤った考えは非常に広まっているため、多くの人が常識と考えています。しかし、それは間違いです。
したがって、OPのこのステートメントは
私が読んだほぼすべてのJavaの本は、オブジェクト間で状態と動作を共有する方法としてインターフェイスを使用することについて話します
それは不可能です。インターフェイスには状態も動作もありません。実装クラスが提供する必要があるプロパティを定義できますが、それは可能な限り近いものになります。インターフェイスを使用して動作を共有することはできません。
メソッドの名前が暗示するような動作を提供するためにインターフェイスを実装するだろうと推測することもできますが、それはまったく同じではありません。また、そのようなメソッドがいつ呼び出されるかについては、まったく制限がありません (たとえば、Start は Stop の前に呼び出される必要があります)。
この文
訪問者パターンなどの GoF タイプのパターンに必須
も間違っています。GoF の本では、当時使用されていた言語の機能ではなかったインターフェイスをまったく使用していません。どのパターンもインターフェイスを必要としませんが、インターフェイスを使用できるものもあります。IMO では、Observer パターンは、インターフェイスがよりエレガントな役割を果たすことができるパターンです (ただし、このパターンは現在、通常、イベントを使用して実装されています)。Visitor パターンでは、ほとんどの場合、訪問ノードの各タイプのデフォルト動作を実装する基本 Visitor クラス (IME) が必要です。
個人的には、この質問に対する答えは 3 つあると思います。
インターフェイスは多くの人にとって特効薬であると考えられています (これらの人々は通常、「契約」に関する誤解の下で苦労しているか、インターフェイスが魔法のようにコードを切り離していると考えています)
Java の人々はフレームワークの使用に非常に重点を置いており、その多くは (当然のことながら) クラスにインターフェイスを実装することを要求します。
ジェネリックスとアノテーション (C# の属性) が導入される前は、インターフェイスはいくつかのことを行うための最良の方法でした。
インターフェイスは非常に便利な言語機能ですが、悪用されることもよくあります。症状には次のようなものがあります。
インターフェイスは 1 つのクラスによってのみ実装されます
クラスは複数のインターフェイスを実装します。インターフェイスの利点としてよく宣伝されますが、通常、問題のクラスが関心の分離の原則に違反していることを意味します。
インターフェイスの継承階層があります (多くの場合、クラスの階層によってミラーリングされます)。これは、そもそもインターフェイスを使用して回避しようとしている状況です。継承が多すぎることは、クラスとインターフェイスの両方にとって悪いことです。
私の意見では、これらすべてはコードの匂いです。
ルーズさを促進する一つの方法です カップリング.
低結合では、1 つのモジュールを変更しても、別のモジュールの実装を変更する必要はありません。
この概念をうまく活用すると、 抽象的な工場パターン. 。Wikipedia の例では、GUIFactory インターフェイスが Button インターフェイスを生成します。具体的なファクトリは、WinFactory (WinButton を生成) または OSXFactory (OSXButton を生成) です。GUI アプリケーションを作成していて、次のすべてのインスタンスを調べなければならない場合を想像してください。 OldButton
クラスを作成し、それらを次のように変更します WinButton
. 。来年は、次のことを追加する必要があります OSXButton
バージョン。
私の意見では、これが頻繁に見られるのは、これが間違った状況で適用されることが多い非常に良い習慣だからです。
インターフェイスには、抽象クラスと比較して多くの利点があります。
- インターフェイスに依存するコードを再構築せずに実装を切り替えることができます。これは次の場合に役立ちます。プロキシ クラス、依存関係注入、AOP など。
- API をコード内の実装から分離できます。これは、他のモジュールに影響を与えるコードを変更していることが明確になるため、便利です。
- これにより、コードに依存するコードを作成する開発者は、テスト目的で API を簡単にモックできます。
コードのモジュールを扱う場合、インターフェイスから最大限の利点が得られます。ただし、モジュール境界をどこに置くかを決定する簡単なルールはありません。したがって、このベスト プラクティスは、特に最初にソフトウェアを設計する場合には、使いすぎてしまいがちです。
(@eed3s9n とともに) 疎結合を促進するためだと思います。また、インターフェイスがないとオブジェクトのモックアップができないため、単体テストがはるかに困難になります。
なぜ拡張するのは悪なのか. 。この記事は、質問に対するほぼ直接的な回答です。実際にそうするケースはほとんど思いつきません 必要 抽象クラスであり、それが悪い考えである状況がたくさんあります。これは、抽象クラスを使用した実装が悪いという意味ではありませんが、インターフェイス コントラクトが特定の実装のアーティファクトに依存しないように注意する必要があります (適切な例:Java の Stack クラス)。
もう一つ:どこにでもインターフェイスを設ける必要はありませんし、推奨される習慣でもありません。通常、インターフェイスが必要な場合とそうでない場合を識別する必要があります。理想的な世界では、ほとんどの場合、2 番目のケースは最終クラスとして実装されるはずです。
ここには素晴らしい答えがいくつかありますが、具体的な理由を探している場合は、単体テスト以外に探す必要はありません。
ビジネス ロジックで、トランザクションが発生する地域の現在の税率を取得するメソッドをテストするとします。これを行うには、ビジネス ロジック クラスがリポジトリ経由でデータベースと通信する必要があります。
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; }
}
ライブ コード (ビジネス クラス) はファクトリを使用してリポジトリを取得するため、単体テストでは 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 と昔ながらのコーディングを組み合わせて使用しています。
ある意味では、あなたの質問は単純に「抽象クラスではなくインターフェイスを使用するのはなぜですか?」技術的には、両方でゆるい結合を実現できます。基礎となる実装はまだ呼び出しコードにさらされておらず、抽象的な工場パターンを使用して基礎となる実装(インターフェイス実装対抽象クラス拡張) を使用して、設計の柔軟性を高めます。実際、抽象クラスを使用すると、コードを満たすために実装を要求する (「start() を実装しなければならない」) ことと、デフォルトの実装を提供する (「標準のペイント() を持っています)」の両方が可能になるため、抽象クラスではさらに多くのことができると主張することもできます。必要に応じてオーバーライドできます。」) -- インターフェースでは実装を提供する必要があり、時間の経過とともにインターフェースの変更による脆弱な継承の問題が発生する可能性があります。
ただし、基本的に、私は主に Java の単一継承制限によりインターフェイスを使用します。私の実装が、コードの呼び出しによって使用される抽象クラスから継承しなければならない場合、それは、たとえそれがより意味があるとしても、他のものから継承する柔軟性を失うことを意味します(例:コードの再利用またはオブジェクト階層のため)。
理由の 1 つは、インターフェイスによって成長と拡張性が可能になることです。たとえば、オブジェクトをパラメータとして受け取るメソッドがあるとします。
パブリックボイドドリンク(コーヒーSomedrink){
}
ここで、まったく同じメソッドを使用するが、hotTea オブジェクトを渡すとします。まあ、それはできません。上記のメソッドを、コーヒー オブジェクトのみを使用するようにハードコードしました。それは良いことかもしれないし、悪いことかもしれない。上記の欠点は、あらゆる種類の関連オブジェクトを渡したい場合に、1 種類のオブジェクトに厳密に制限されてしまうことです。
IHotDrink などのインターフェイスを使用すると、
インターフェイス IHotDrink { }
オブジェクトの代わりにインターフェースを使用するように上記のメソッドを書き換えます。
public void drink(ihotdrinksomedrink){
}
これで、IHotDrink インターフェイスを実装するすべてのオブジェクトを渡すことができます。確かに、異なるオブジェクト パラメーターを使用してまったく同じことを行うまったく同じメソッドを作成することはできますが、なぜでしょうか?突然肥大化したコードを保守することになります。
コーディングの前にデザインすることがすべてです。
インターフェイスを指定した後で 2 つのオブジェクト間の関係がすべて分からない場合は、インターフェイスの定義が不十分であることになります。これは比較的簡単に修正できます。
コーディングに直接取り組んで、途中で何かが欠けていることに気づいた場合、修正するのははるかに困難です。
これは Perl/Python/Ruby の観点から見ることができます。
- オブジェクトをパラメータとしてメソッドに渡すとき、そのオブジェクトの type を渡すのではなく、オブジェクトがいくつかのメソッドに応答する必要があることだけを知っています。
Java インターフェースをこれに例えて考えると、これが最もよく説明されると思います。実際には type を渡すのではなく、メソッドに応答するもの (トレイト、つまり trait ) を渡すだけです。
Java でインターフェイスを使用する主な理由は、単一継承の制限にあると思います。多くの場合、これにより不必要な複雑化やコードの重複が発生します。Scala の Traits を見てみましょう。 http://www.scala-lang.org/node/126 特性は特殊な種類の抽象クラスですが、クラスは特性の多くを拡張できます。