それでも同じことはできません! -長期設計のためのインターフェースと依存性注入の使用

みなさんこんにちは!



最終的に、Mark Simanの本「 .NETでの依存性注入 」を更新する契約を結んでいます 。主なことは、彼ができるだけ早くそれを完成させることです。 編集部には尊敬されるDinesh RajputによるSpring 5のデザインパターンに関する本もあります。この章では、依存関係の実装に関する章もあります。



私たちは、DIパラダイムの長所を思い起こさせ、それに対する関心を明確にする興味深い資料を長い間探していましたが、今では発見されています。 確かに、著者はGoで例を提供することを好みました。 このトピックがあなたに近い場合、これがあなたの彼の考えに従うことを妨げず、コントロールの反転とインターフェースの操作の一般原則を理解するのに役立つことを願っています。



オリジナルの感情的な色付けは少し静かで、翻訳中の感嘆符の数が減ります。 素敵な読書を!



インターフェイスの使用は理解しやすい手法であり、テストでテストしやすく拡張しやすいコードを作成できます。 これが最も強力なアーキテクチャ設計ツールであると繰り返し確信しています。



この記事の目的は、インターフェースとは何か、それらがどのように使用されるか、そしてそれらがどのようにコードの拡張性とテスト容易性を提供するかを説明することです。 最後に、この記事では、インターフェイスがソフトウェア配信管理を最適化し、計画を簡素化する方法を示す必要があります



インターフェース



インターフェイスは契約を記述します。 言語またはフレームワークに応じて、インターフェースの使用は明示的または暗黙的に指示される場合があります。 したがって、Go言語では、 インターフェイスは明示的に指示されます 。 エンティティをインターフェイスとして使用しようとしたが、このインターフェイスのルールと完全に一致しない場合、コンパイル時エラーが発生します。 たとえば、上記の例に続いて、次のエラーが表示されます。



prog.go:22:85: cannot use BadPricer literal (type BadPricer) as type StockPricer in argument to isPricerHigherThan100: BadPricer does not implement StockPricer (missing CurrentPrice method) Program exited.
      
      





インターフェイスは、呼び出し元を呼び出し先から切り離すのに役立つツールです。これは、コントラクトを使用して行われます。



自動交換取引のプログラムの例を使用して、この問題を特定しましょう。 トレーダープログラムは、設定された購入価格とティッカーシンボルで呼び出されます。 その後、プログラムは取引所に行き、このティッカーの現在の相場を見つけます。 さらに、このティッカーの購入価格が設定価格を超えない場合、プログラムは購入を行います。







簡略化した形式では、このプログラムのアーキテクチャは次のように表すことができます。 上記の例から、現在の価格を取得する操作は、プログラムが交換サービスに接続するためのHTTPプロトコルに直接依存することが明らかです。



Action



の状態もHTTPに直接依存しています。 したがって、両方の状態は、HTTPを使用して交換データを抽出する方法やトランザクションを完了する方法を完全に理解する必要があります。



実装は次のようになります。



 func analyze(ticker string, maxTradePrice float64) (bool, err) { resp, err := http.Get( "http://stock-service.com/currentprice/" + ticker ) if err != nil { //   } defer resp.Body.Close() body, err := ioutil.ReadAll(resp.Body) // ... currentPrice := parsePriceFromBody(body) var hasTraded bool var err error if currentPrice <= maximumTradePrice { err = doTrade(ticker, currentPrice) if err == nil { hasTraded = true } } return hasTraded, err }
      
      





ここでは、呼び出し元( analyze



)はHTTPに直接依存しています。 彼女は、HTTP要求がどのように定式化されるかを知る必要があります。 解析はどのように行われますか。 再試行、タイムアウト、認証などの処理方法 彼女はhttp



よく理解していhttp



analyzeを呼び出すたびに、 http



ライブラリも呼び出す必要があります




ここでインターフェイスはどのように役立ちますか? インターフェイスによって提供されるコントラクトでは、特定の実装ではなく、 動作を説明できます。



 type StockExchange interface { CurrentPrice(ticker string) float64 }
      
      





上記は、 StockExchange



の概念を定義しています。 ここでは、 StockExchange



が唯一のCurrentPrice



関数の呼び出しをサポートしていると述べています。 これらの3行は、私にとって最も強力な建築技術のようです。 アプリケーションの依存関係をより自信を持って制御するのに役立ちます。 テストを提供します。 拡張性を提供します。



依存性注入



インターフェイスの価値を完全に理解するには、「依存性注入」と呼ばれる手法を習得する必要があります。



依存性注入とは、呼び出し側が呼び出し側が必要とするものを提供することを意味します。 通常、これは次のようになります。呼び出し元はオブジェクトを構成し、呼び出し先に渡します。 次に、着信側が構成と実装から抽象化します。 この場合、既知の調停があります。 HTTP Restサービスへのリクエストを検討してください。 クライアントを実装するには、HTTP要求を作成、送信、受信できるHTTPライブラリを使用する必要があります。



HTTP要求をインターフェイスの背後に配置した場合、呼び出し元は切り離される可能性があり、HTTP要求が実際に行われたことを「知らない」ことになります。



呼び出し元は、一般的な関数呼び出しのみを行う必要があります。 これは、ローカルコール、リモートコール、HTTPコール、RPCコールなどです。 発信者は、何が起こっているのかを知らず、通常、期待される結果が得られるまで完璧に彼女に合っています。 以下は、 analyze



メソッドで依存性注入がどのように見えるかを示しています。



 func analyze(se StockExchange, ticker string, maxTradePrice float64) (bool, error) { currentPrice := se.CurrentPrice(ticker) var hasTraded bool var err error if currentPrice <= maximumTradePrice { err = doTrade(ticker, currentPrice) if err == nil { hasTraded = true } } return hasTraded, err }
      
      





ここで起こっていることに驚かされることはありません。 依存関係ツリーを完全に反転し、プログラム全体をより適切に制御し始めました。 さらに、視覚的にも実装全体がよりクリーンで理解しやすくなりました。 分析方法が現在の価格を選択し、この価格が私たちに適しているかどうかを確認し、そうである場合は取引を行う必要があることを明確に確認します。



最も重要なことは、この場合、呼び出し元から呼び出し元を切り離します。 呼び出し元と実装全体は、インターフェイスを使用して呼び出されたものから分離されているため、さまざまな実装を作成してインターフェイスを拡張できます。 インターフェイスを使用すると、着信側のコードを変更することなく、さまざまな特定の実装を作成できます。







このプログラムの「現在の価格を取得」ステータスは、 StockExchange



インターフェースのみに依存します。 この実装は、交換サービスへの連絡方法、価格の保存方法、またはリクエストの作成方法について何も知りません。 本当に至福の無知。 さらに、二国間。 HTTPStockExchange



実装も分析について何も知りません。 課題が間接的であるため、分析が実行されるときに、分析が実行されるコンテキストについて。



特定の実装を変更/追加/削除する場合、プログラムフラグメント(インターフェイスに依存するもの)を変更する必要がないため、 このような設計は耐久性があります。 StockService



非常に頻繁に利用できないことがStockService



ます。



上記の例は関数の呼び出しとどのように違いますか? 関数呼び出しを使用すると、実装もよりクリーンになります。 違いは、関数を呼び出すときに、HTTPに頼らなければならないことです。 analyze



メソッドは、 http



自体を直接呼び出すのではなく、単にhttp



を呼び出す必要がある関数のタスクを委任します。 この手法の強みは、「注入」、つまり呼び出し元が呼び出し先にインターフェイスを提供することにあります。 これは、依存関係の反転が発生する方法とまったく同じです。この場合、取得価格は実装に依存せず、インターフェイスにのみ依存します。



すぐに使える複数の実装



この段階では、 analyze



関数とStockExchange



インターフェイスがありますが、実際には便利なことは何もできません。 プログラムを発表しました。 現時点では、インターフェイスの要件を満たす単一の特定の実装がないため、これを呼び出すことはできません。



次の図の主な強調点は、「現在の価格の取得」状態とStockExchange



インターフェースへの依存です。 以下は、2つの完全に異なる実装が共存し、現在の価格を取得する方法が不明であることを示しています。 さらに、両方の実装は相互に関連しておらず、それぞれはStockExchange



インターフェースのみに依存してStockExchange



ます。







生産



元のHTTP実装は、プライマリanalyze



実装にすでに存在します。 それを抽出して、インターフェースの特定の実装の背後にカプセル化するだけです。



 type HTTPStockExchange struct {} func (se HTTPStockExchange) CurrentPrice(ticker string) float64 { resp, err := http.Get( "http://stock-service.com/currentprice/" + ticker ) if err != nil { //   } defer resp.Body.Close() body, err := ioutil.ReadAll(resp.Body) // ... return parsePriceFromBody(body) }
      
      





以前に関数analyzeにリンクしていたコードは自律型になり、 StockExchange



インターフェイスを満たします。つまり、 analyze



渡すことができるようになりました。 上記の図から覚えているように、analyzeはHTTP依存関係に関連付けられなくなりました。 インターフェースを使用して、 analyze



は舞台裏で何が起こるかを「想像」しません。 彼は、 CurrentPrice



を呼び出すことができるオブジェクトが与えられることが保証されることだけを知っています。



また、ここではカプセル化の典型的な長所を利用しています。 以前は、httpリクエストが分析に結び付けられていた場合、httpを介して取引所と通信する唯一の方法は、 analyze



メソッドを介して間接的でした。 はい、これらの呼び出しを関数にカプセル化し、関数を独立して実行できますが、インターフェイスにより、呼び出し元から呼び出し元を切り離す必要があります。 これで、呼び出し元に関係なくHTTPStockExchange



をテストできます。 これは、テストの範囲と、テストの失敗を理解して対応する方法に根本的に影響します。



テスト中



既存のコードには、 HTTPStockService



構造があります。これにより、Exchangeサービスと通信し、 HTTPStockService



受信した応答を解析できることを個別に確認できます。 しかし、今度は、analyzeがStockExchange



インターフェースからの応答を正しく処理できること、さらに、この操作が信頼でき、再現可能であることを確認しましょう。



 currentPrice := se.CurrentPrice(ticker) if currentPrice <= maxTradePrice { err := doTrade(ticker, currentPrice) }
      
      





HTTPを使用した実装を使用できますが、多くの欠点があります。 ユニットテストでネットワーク呼び出しを行うと、特に外部サービスの場合は遅くなる場合があります。 遅延と不安定なネットワーク接続のために、テストは信頼できないことが判明する可能性があります。 さらに、トランザクションを完了できるステートメントを使用したテスト、およびトランザクションを終了すべきでないケースを除外できるステートメントを使用したテストが必要な場合、これらの両方を確実に満たす実際の本番データを見つけることは困難です条件。 maxTradePrice



選択することもできます。たとえば、 maxTradePrice := -100



場合、この条件で各条件を人為的に模倣します。トランザクションは完了せず、 maxTradePrice := 10000000



は明らかにトランザクションで終了します。



しかし、交換サービスで特定のクォータを取得するとどうなりますか? または、アクセス料金を支払う必要がある場合 単体テストに関しては、クォータを実際に支払うか、使うべきでしょうか? 理想的には、テストはできるだけ頻繁に実行する必要があります。そのため、テストは高速で、安価で、信頼できるものでなければなりません。 この段落から、純粋なHTTPを備えたバージョンを使用することがテストの観点から非合理的である理由は明らかだと思います!



より良い方法があり、インターフェースを使用する必要があります!



インターフェースがあれば、 StockExchange



実装を慎重に製造できます。これにより、迅速、安全、確実にanalyze



することができます。



 type StubExchange struct { Price float64 } func (se StubExchange) CurrentPrice(ticker string) float64 { return se.Price } func TestAnalyze_MakeTrade(t *testing.T) { se := StubExchange{Price: 10} maxTradePrice := 11 traded, err := analyze(se, "TSLA", maxTradePrice) if err != nil { t.Errorf("expected err == nil received: %s", err) } if !traded { t.Error("expected traded == true") } } func TestAnalyze_DontTrade(t *testing.T) { se := StubExchange{Price: 10} maxTradePrice := 9 traded, err := analyze(se, "TSLA", maxTradePrice) //  }
      
      





交換サービスのスタブが上記で使用されているanalyze



analyze



となるブランチが起動されます。 次に、分析が必要なことを実行することを確認するために、各テストでステートメントが作成されます。 これはテストプログラムですが、私の経験では、インターフェイスがほぼこのように使用されるコンポーネント/アーキテクチャは、バトルコードの耐久性についてもこの方法でテストされることを示唆しています!!! インターフェースのおかげで、メモリ制御のStockExchange



使用できます。これは、信頼性が高く、設定が容易で、理解しやすく、再現性があり、超高速のテストを提供します!!!



固定解除-発信者設定



インターフェイスを使用して呼び出し元を呼び出し先から切り離す方法、および複数の実装を行う方法について説明したので、重要な点についてはまだ触れていません。 厳密に定義された時間に特定の実装を構成および提供する方法は? 分析関数を直接呼び出すことができますが、実稼働構成で何をする必要がありますか?



これは、依存関係の実装が役立つ場所です。



 func main() { var ticker = flag.String("ticker", "", "stock ticker symbol to trade for") var maxTradePrice = flag.Float64("maxtradeprice", "", "max price to pay for a share of the ticker symbol." se := HTTPStockExchange{} analyze(se, *ticker, *maxTradePrice) }
      
      





テストケースと同様に、 analyze



で使用されるStockExchangeの具体的な実装は、analyze以外の呼び出し元によって構成されます。 次に、 analyze



渡されます(注入されます)。 これにより、 HTTPStockExchange



どのように構成されているかについてNOTHINGが分析されているHTTPStockExchange



ます。 おそらく、コマンドラインフラグとして使用するhttpドメインを提供したいので、analyzeを変更する必要はありません。 または、環境から抽出されるHTTPStockExchange



にアクセスするために何らかの認証またはトークンを提供する必要がある場合はどうでしょうか? 繰り返しますが、分析は変更しないでください。



設定はanalyze



以外のレベルで行われるanalyze



、analyzeは独自の依存関係を設定する必要がありません。 したがって、職務の厳密な分離が達成されます。







棚の決定



おそらく上記の例で十分ですが、インターフェースや依存性注入には他にも多くの利点があります。 インターフェイスにより、特定の実装に関する決定を延期することができます。 決定には、サポートする動作を決定する必要がありますが、特定の実装については後で決定できます。 自動トランザクションを行いたいと思っていたが、どの見積プロバイダを使用するかまだわからなかったとします。 データウェアハウスを使用する場合、同様のクラスのソリューションが常に扱われます。 私たちのプログラムは何を使うべきですか:mysql、postgres、redis、ファイルシステム、cassandra? 最終的に、これはすべて実装の詳細であり、インターフェイスにより、これらの問題に関する最終決定を延期できます。 これにより、プログラムのビジネスロジックを開発し、最後に特定の技術ソリューションに切り替えることができます!



この手法だけでも多くの可能性が残されているという事実にもかかわらず、プロジェクト計画のレベルで何か不思議なことが起こります。 Exchangeインターフェイスに別の依存関係を追加するとどうなるか想像してみてください。







ここでは、非循環有向グラフの形式でアーキテクチャを再構成します。これにより、交換インターフェイスの詳細に同意するとすぐに、 HTTPStockExchange



を使用して、パイプラインをHTTPStockExchange



し続けることができます。 私たちは、プロジェクトに新しい人を追加することで、より速く動くことができる状況を作り出しました。 このようにアーキテクチャを微調整することで、プロジェクト全体の配信を促進するために、プロジェクトに追加の人をどこで、いつ、どのくらいの期間従事させることができるかをよりよく把握できます。 さらに、インターフェイス間の接続が弱いため、通常、実装インターフェイスから始めて作業に参加するのは簡単です。 プログラムとは完全に独立して、 HTTPStockExchange



開発、テスト、テストできます!



アーキテクチャの依存関係を分析し、これらの依存関係に基づいて計画することで、プロジェクトを大幅に加速できます。 この特定の手法を使用して、数か月間割り当てられたプロジェクトを非常に迅速に完了することができました。



先に



これで、インターフェイスと依存関係の注入により、設計されたプログラムの耐久性がどのように保証されるかがより明確になります。 見積プロバイダーを変更するか、クォータのストリーミングを開始してリアルタイムで保存するとします。 あなたが好きなだけ他の多くの可能性があります。 現在の形式のanalyzeメソッドは、 StockExchange



インターフェイスとの統合に適した実装をサポートします。



 se.CurrentPrice(ticker)
      
      





したがって、多くの場合、変更せずに実行できます。 すべてではありませんが、私たちが遭遇する可能性のある予測可能なケースでは。 analyze



コードを変更し、その主要機能を再確認analyze



必要性から免れるだけでなく、新しい実装を簡単に提供したり、サプライヤを切り替えたりすることができます。 また、 analyze



を変更したりダブルチェックしたりすることなく、すでにある特定の実装をスムーズに拡張または更新できます!



上記の例が、インターフェイスを使用してプログラム内のエンティティ間の接続を弱めると、依存関係が完全に再構築され、呼び出し元と呼び出し元が分離されることを納得できるように示してくれることを期待しています。 この分離のおかげで、プログラムは特定の実装に依存しませんが、特定の動作に大きく依存します 。 この動作は、さまざまな実装で提供できます。 この重要な設計原則は、 カモタイピングとも呼ばれます



インターフェースという概念と、実装ではなく振る舞いへの依存は非常に強力なので、インターフェースを言語プリミティブと見なします-はい、これは非常に急進的です。 上記の例が非常に説得力のあるものになり、プロジェクトの最初からインターフェースと依存関係の注入を使用することに同意することを願っています。 私が取り組んだほとんどすべてのプロジェクトでは、1つではなく、少なくとも2つの実装(実稼働用とテスト用)が必要でした。



All Articles