1. 序章

ソフトウェア開発者としてのプロとしてのキャリアの初めに、私たちのほとんどは実装をプログラムします。

後で、直感的に、または必要性のために、私たちはゆっくりとこの考え方を変える傾向があります。 抽象化とインターフェースにますます多くのコードを記述します。

このチュートリアルでは、これらの用語の意味と、それらの長所と短所を確認します。

2. ケーススタディ

さまざまなテクニックを示すために、いくつかの簡単なタスクについて説明します。 ゲームシリーズBigCarStealingを覚えていますか? 私たちは幸運にも最新バージョンの開発者になることができました。

2.1. 実装を一緒に配線する

解決する最初のタスクは、キャラクターに車を運転させることです。 私たちの雇用主は、私たちが見ている最善の方法で状況を処理するための絶対的な信頼を私たちに与えました。

最初の概念実証では、スピードアップとスローダウンのみに焦点を当てています。

私たちの最初の直感は、PlayerCarの2つのクラスを作成することです。

必要なのは、 Playerクラスのdrive()メソッドを呼び出し、Carインスタンスを渡すことだけです。

私たちはすべてを実装し、それは魅力のように機能します。 ただし、次のタスクがあります。 車の運転に加えて、トラックの運転もサポートする必要があります。

問題ありません。TruckクラスとPlayer.drive(Truck)メソッドを紹介します。

ここまでは順調ですね。 しかしその後、私たちは次の課題に直面します。それはボートの運転です。 (待って、何? ボート? ゲームは車に関するものだと思いました。 変。)

トラックで行ったのと同じプロセスを繰り返すことができます。 しかし、私たちは疑問に思い始めます。他にいくつのものを運転できるでしょうか。 バックログを見ると、31種類の運転可能なもの(自転車、飛行機、潜水艦、ジェットパック、さらには宇宙ステーションを含む)が登場していることがわかります。

したがって、別のアプローチが必要です。 どうすればもっとうまくできるでしょうか?

2.2. 抽象化の使用

救助への抽象化! 抽象Vehicleクラスを作成することにしました。これは、Car、Truck、Boat、および将来のすべてのクラスのスーパークラスになります。

また、この方法では、 Playerクラスに単一のdrive(Vehicle)メソッドが必要です。

次のタスクは、ゲーム内の事故を処理することです。 これらの事故の間、車両は損傷を受けます。 これをサポートするために、新しいメソッドを導入します。

ただし、メソッド名 break() broken()は非常に似ているため、それらを混同し、イライラするデバッグセッションを引き起こしました。 最終的に問題の原因が見つかったら、質問を提案します。 不要なメソッドをどうにかして隠すことができますか?

DriveableBreakableというさまざまなシナリオ用に複数の基本クラスを作成するというアイデアが得られました。 大学でBDecreasedプログラミング言語についてのクラスを受講しました。これにより、多重継承が可能になりました。 ただし、現在はHotBrownStuff言語を使用していますが、これは(正当な理由で)それをサポートしていません。 では、どうすれば先に進むことができますか?

2.3. 複数の抽象化の使用

HotBrownStuffには、BDecreasedに対する新しい概念であるインターフェイスがあります。 クラスは複数のインターフェースを実装できます。これにより、次のクラス図で問題を解決できます。

新しい機能が登場するたびにリファクタリングを行う必要があるため、燃え尽き症候群に近づいています。 しかし、開発を続ける必要があるので、次の課題を取得します。建物を車両で攻撃すると、建物にも損傷が発生するはずです。 深呼吸をして、コードベースの半分をもう一度書き直す準備をします。

しかし、それを解決することを考えると、簡単な実装が見つかります。

建物にBreakableインターフェースを実装させます。

驚いたことに、多くの労力をかけなくてもすべてが正常に機能しています。 鳥はまた鳴き声を上げています。 太陽が輝いている。 また笑顔になります。 より良い建築設計のためにこれらすべてに感謝することができます。

2.4. ケーススタディの要約

私たちが使用したテクニックの違いは何でしたか?

まず、他のクラスの実装クラスを直接使用しました。 通常、このメソッドを「クラスへのプログラミング」または「実装へのプログラミング」と呼びます。

異なるクラス間に多くの依存関係があるため、コードは緊密に結合されます。 これにより、コードが壊れやすくなります。これは、コードの一部を変更すると、予期しない多くの場所で問題が発生する傾向があるためです。

次に、抽象クラスを導入しました。これにより、クラスのクライアントが具体的な実装から切り離されました。 この手法を「抽象化へのプログラミング」と呼びます。

しかし、同じ抽象化で機能のさまざまな側面を混合していました。

最後に、複数の抽象化、つまりインターフェイスを紹介しました。 このメソッドを「インターフェースへのプログラミング」と呼びます。 インターフェイスも抽象化であることに注意してください。 したがって、このメソッドは、抽象化へのプログラミングのサブセットです。

インターフェイスを使用すると、デカップリングの実装に加えて、複数の概念をデカップリングすることができました。

一言で言えば、インターフェイスにプログラミングする場合、さまざまなビジネスロジック部分は実装を介して接続されません。 それらはインターフェースを介して接続されています。

3. 結果

短所から始めましょう。 インターフェイス、クラス、場合によっては抽象クラスなど、はるかに多くのタイプを作成する必要があります。 最初は圧倒されるかもしれませんが、適切なフォルダ/パッケージ構造を使用すれば、これを管理できます。

また、実装をインスタンス化するための外部コンポーネントが必要になります。 できれば、ビジネスロジックではなく、インフラストラクチャにあります。 デザインパターンに関するセクションで、このトピックに戻ります。

しかし、それはプロと比較して小さな価格です。 これらの利点については、次のセクションで詳しく説明します。

3.1. 統一された方法

車、トラック、ボートの例を考えてみてください。 これらすべてに同じメソッド名を使用しましたが、共通の祖先がなくても、メソッドに簡単に異なる名前を付けることができました。 たとえば、 Accelerate() speedUp()、および goFaster()はすべて、同じ機能に名前を付けるための有効な候補です。 異なるクラスでそれらを混ぜることができます。 たとえば、車が加速し、トラックがスピードアップし、ボートが速くなる可能性があります。

抽象化により、クラス間のコントラクトを宣言します。 契約には、実装がクライアントに提供する操作の種類が記載されています。 ただし、これらの操作がどのように機能しているかについては何も述べていません。これは良いことです。 このようにして、私たちはそれをどのように行うかではなく、私たちがやりたいことに集中することができます。

3.2. 実装を非表示にする

インターフェイスのみがビジネスロジックの他の部分に表示されます。 これらのインターフェースを小さく、シンプルに、そしてまとまりを増すためにまっすぐに保つように努力する必要があります。 言い換えると、抽象化により、アプリケーションのさまざまな部分の間に境界を導入します。

これを行うと、実装の詳細が誤って漏洩することはありません。これにより、異なるコンポーネント間に緊密な結合が生じる傾向があります。 これにより、リファクタリングと変更が困難になります。 また、コードが理解しにくくなります。

その上、実装クラスは(一見)見えないので、抽象化(したがって、コントラクト)だけが見えます。 したがって、コードのロジックをより簡単に理解できます。 繰り返しになりますが、クラスがどのように行うかではなく、クラスが行うことに焦点を当てることができます。 また、数十のクラスの名前と責任を覚えておく必要はありません。 抽象化はそれらすべての詳細を隠します。

たとえば、JDBC APIは、多くのインターフェイスと1つのクラスを定義します。 JDBCドライバーは、これらのインターフェースを実装します。 ただし、アプリケーションコードのこれらのクラスは使用しません。 コアJDBCタイプのみを使用します。

3.3. 妥当性

シラミの結合と責任の軽減により、コードのテストが容易になります。

インターフェイスに依存しているため、具体的な実装ではなく、テストダブルに簡単に合格できます。 また、これらのインターフェースはより小さく、明確な責任があるため、それらにモックを提供するのは簡単です。

3.4. 複数の実装の紹介

クライアントコードを変更せずに新しい実装を導入できることもわかりました。詳細を自由に拡張できるため、これは強力な概念です。 新しい要件がある場合は、古い実装を削除して新しい実装に置き換えることができます。 たとえば、データアクセス層を抽象化すると、ビジネスロジックを変更せずにSQLデータベースからグラフデータベースに切り替えることができます。

JDBCもこの概念に依存しています。 別のデータベースエンジンを使用することにした場合、必要なのはJDBCドライバーを置き換えることだけです。 アプリケーションコードは、実装クラスから独立しているため、同じままです。

4. SOLIDの原則への接続

インターフェイスにプログラミングすると、複数のSOLIDの原則に従うことが容易になります。

  • 単一責任の原則:小さなインターフェースを作成することにより、クラスを実装するための明白な責任を定義します。 特に、クラスにほんの一握りのインターフェイスまたは単一のインターフェイスを実装させる場合は、SRPの追跡が容易になります。
  • オープン/クローズド原則:緩い結合と隠された実装により、OCPに従うこともより簡単になります。 クライアントコードは実装に依存しないため、必要に応じて追加のサブクラスを導入できます。
  • リスコフの置換原則:LSPはこの手法に直接接続されていません。 ただし、この原則に従うように継承階層を設計するときも注意する必要があります。
  • インターフェイス分離の原則:ISPは結果ではありませんが、インターフェイスをプログラミングするときに従うべき良い習慣です。 小さく明確に定義された責任を定義することの重要性については、すでに説明したことに注意してください。 これらのメモは、ISPをフォローするための隠されたヒントでした。
  • 依存性逆転の原則抽象化に依存することで、DIPに従う作業の大部分をすでに実行しました。最後に行うことは、依存関係をインスタンス化するのではなく、外部からの依存関係を期待することです。初めの。

インターフェイスのみに依存しているため、この最後のステップはすでに推測されていることに注意してください。 クラスに依存せずにクラスをインスタンス化することはできません。

5. 関連するデザインパターン

実装をインスタンス化するには、いくつかのインフラストラクチャが必要であることはすでに説明しました。 通常、この問題は、ファクトリ、ファクトリメソッド、静的ファクトリメソッド、抽象ファクトリ、およびビルダーパターンを使用して解決します。 依存性注入を使用すると(できれば Spring CDI Guice などのフレームワークを介して)、これが簡単になります。

もちろん、要件によっては、これらのパターンのいくつかを混在させる場合があります。 それは目前の正確な問題に依存します。

前のパターンは、インターフェースのプログラミングを容易にします。 コインの反対側では、いくつかのパターンは抽象化に依存しています。 したがって、インターフェースにプログラミングすると、インターフェースが使いやすくなります。 いくつかの例は、 Adapter Composite Decorator Proxy Mediator Observerです。 ]、状態戦略、およびビジター

6. 結論

インターフェイスとクラスの数が多いため、インターフェイスへのプログラミングは初心者には面倒に見えます。 また、多くの依存関係階層を導入することも、最初は奇妙かもしれません。

ただし、私たちはOOの優れた実践と原則に従っています。 インターフェイスにプログラミングすることで、アプリケーションが緩く結合され、より拡張可能で、よりテスト可能で、より柔軟になり、理解しやすくなります。 それをマスターするには時間と練習が必要ですが、努力する価値があります。