依存性注入とパブリックAPIデザインのバランス


13

私は、依存性注入を使用したテスト可能な設計と、単純な固定パブリックAPIの提供とのバランスを取る方法を検討してきました。私のジレンマは次のとおりです。人々はvar server = new Server(){ ... }多くの依存関係や依存関係のグラフを作成することについて心配する必要はなく、何かをしたいと思うでしょうServer(,,,,,,)。開発中は、IoC / DIフレームワークを使用してすべてを処理するため、あまり心配する必要はありません(コンテナのライフサイクル管理の側面を使用していないため、さらに複雑になります)。

現在、依存関係が再実装されることはほとんどありません。この場合のコンポーネント化は、拡張などの継ぎ目を作成するのではなく、ほとんど純粋にテスト容易性(および適切な設計!)のためです。99.999%の人は、デフォルト構成を使用したいと思うでしょう。そう。依存関係をハードコーディングできました。それをしたくない、テストを失う!ハードコードされた依存関係と依存関係を取得するデフォルトのコンストラクターを提供できます。それは...乱雑で、混乱を招く可能性がありますが、実行可能です。依存関係を受け取るコンストラクタを内部に作成し、ユニットテストでフレンドアセンブリ(C#を想定)を作成できます。これにより、パブリックAPIは整頓されますが、メンテナンスのために厄介な隠されたトラップが残ります。私の本では、明示的にではなく暗黙的に接続された2つのコンストラクターが一般的に悪い設計になります。

現時点では、私が考えることができる最小の悪についてです。ご意見?知恵?

回答:


11

依存性注入は、適切に使用すると強力なパターンになりますが、その実践者は外部フレームワークに依存するようになることが多すぎます。結果として生じるAPIは、アプリをXMLダクトテープと緩やかに結び付けたくない人にとって非常に苦痛です。Plain Old Objects(POO)を忘れないでください。

まず、フレームワークを使用せずに、誰かがAPIを使用することをどのように期待するかを検討してください。

  • サーバーをどのようにインスタンス化しますか?
  • サーバーをどのように拡張しますか?
  • 外部APIで何を公開しますか?
  • 外部APIで何を隠しますか?

コンポーネントにデフォルトの実装を提供するというアイデアと、それらのコンポーネントがあるという事実が気に入っています。次のアプローチは、完全に有効で適切なオブジェクト指向のプラクティスです。

public Server() 
    : this(new HttpListener(80), new HttpResponder())
{}

public Server(Listener listener, Responder responder)
{
    // ...
}

コンポーネントのデフォルト実装がAPIドキュメントにあることを文書化し、すべてのセットアップを実行する1つのコンストラクターを提供する限り、問題ありません。カスケードコンストラクターは、セットアップコードが1つのマスターコンストラクターで発生する限り脆弱ではありません。

基本的に、公開されているすべてのコンストラクターには、公開するコンポーネントのさまざまな組み合わせがあります。内部的には、デフォルトを設定し、セットアップを完了するプライベートコンストラクターに従います。本質的に、これはいくらかの乾燥を提供し、テストは非常に簡単です。

friendサーバーオブジェクトをセットアップしてテスト用に分離するためにテストへのアクセスを提供する必要がある場合は、それを選択します。これはパブリックAPIの一部ではないため、注意が必要です。

APIコンシューマーに親切に対応し、ライブラリを使用するためにIoC / DIフレームワークを必要としません。苦痛を感じさせるために、ユニットテストもIoC / DIフレームワークに依存しないようにしてください。(個人的な苦労ですが、フレームワークを導入するとすぐに、単体テストではなくなり、統合テストになります)。


私は同意し、IoC構成スープのファンではないことは確かです。XML構成は、IoCが関係するところではまったく使用するものではありません。確かに、IoCの使用を要求することは、私が避けようとしていたことです-これは、パブリックAPIデザイナーが決して行うことのできない一種の仮定です。コメントをありがとう、それは私が考えていた行に沿っている、そしてそれは時々健全性チェックを持つことを安心させる!
kolektiv

-1この回答は基本的に「依存性注入を使用しないでください」と言っており、不正確な仮定が含まれています。この例では、HttpListenerコンストラクターが変更された場合、Serverクラスも変更する必要があります。それらは結合されています。より良い解決策は、@ Winston Ewertが以下に述べることです。これは、ライブラリのAPIの外部で、事前に指定された依存関係を持つデフォルトクラスを提供することです。そうすれば、プログラマーはあなたが決定を下すので、すべての依存関係をセットアップすることを強制されませんが、後でそれらを変更する柔軟性をまだ持っています。サードパーティの依存関係がある場合、これは大きな問題です。
M.ダドリー

提供された例では、「ろくでなし注入」と呼ばれます。stackoverflow.com/q/2045904/111327 stackoverflow.com/q/6733667/111327
M.ダドリー

1
@ MichaelDudley、OPの質問を読みましたか。すべての「仮定」は、OPが提供した情報に基づいていました。しばらくの間、あなたがそれを呼んだ「ろくでなしの注入」は、特に実行中に構成が変更されないシステムのために、好まれた依存性注入形式でした。セッターとゲッターも依存性注入の別の形式です。OPが提供する内容に基づいた例を使用しました。
ベリンロリチュ

4

このような場合のJavaでは、通常、工場によって構築された「デフォルト」構成があり、実際の「適切に」動作しているサーバーから独立しています。したがって、ユーザーは次のように記述します。

var server = DefaultServer.create();

ながらServerのコンストラクタは、まだすべての依存関係を受け入れ、深いカスタマイズのために使用することができます。


+ 1、SRP。歯科医院の責任は、歯科医院を建設することではありません。サーバーの責任はサーバーを構築することではありません。
R.シュミッツ

2

「パブリックAPI」を提供するサブクラスを提供してはどうですか

class StandardServer : Server
{
    public StandardServer():
        this( depends1, depends2, depends3)
    {
    }
}

ユーザーはnew StandardServer()途中で移動できます。サーバーの機能をより詳細に制御したい場合は、サーバーの基本クラスを使用することもできます。これは多かれ少なかれ私が使用するアプローチです。(まだ要点を見ていませんので、フレームワークは使用しません。)

これはまだ内部APIを公開していますが、私はあなたがすべきだと思います。オブジェクトを、独立して機能するさまざまな便利なコンポーネントに分割しました。サードパーティがコンポーネントの1つを単独で使用するタイミングを判断することはできません。


1

依存関係を受け取るコンストラクターを内部にし、ユニットテストでフレンドアセンブリ(C#を想定)を作成することができます。これにより、パブリックAPIが整理されますが、メンテナンスのために厄介な隠されたトラップが残ります。

それは私にとって「厄介な隠された "」のようには見えません。パブリックコンストラクタは、「デフォルト」の依存関係を持つ内部コンストラクタを呼び出すだけです。パブリックコンストラクターを変更してはならないことが明確である限り、すべて問題ありません。


0

私はあなたの意見に完全に同意します。単体テストのためだけに、コンポーネントの使用境界を汚染したくありません。これは、DIベースのテストソリューションの問題全体です。

InjectableFactory / InjectableInstanceパターンを使用することをお勧めします。InjectableInstanceは、可変値ホルダーである単純な再利用可能なユーティリティクラスです。コンポーネントインターフェイスには、デフォルトの実装で初期化されたこのValue Holderへの参照が含まれています。インターフェイスは、ValueHolder に委任するシングルトンメソッドget()を提供します。コンポーネントクライアントは、newの代わりにget()メソッドを呼び出して、実装のインスタンスを取得し、直接的な依存関係を回避します。テスト時に、オプションでデフォルトの実装をモックに置き換えることができます。次に、TimeProviderの例を1つ使用しますDate()の抽象化であるクラス。これにより、単体テストで注入とモックを行い、異なる瞬間をシミュレートできます。申し訳ありませんが、私はJavaに精通しているため、ここでjavaを使用します。C#も同様でなければなりません。

public interface TimeProvider {
  // A mutable value holder with default implementation
  InjectableInstance<TimeProvider> instance = InjectableInstance.of(Impl.class);  
  static TimeProvider get() { return instance.get(); }  // Singleton method.

  class Impl implements TimeProvider {        // Default implementation                                    
    @Override public Date getDate() { return new Date(); }
    @Override public long getTimeMillis() { return System.currentTimeMillis(); }
  }

  class Mock implements TimeProvider {   // Mock implemention
    @Setter @Getter long timeMillis = System.currentTimeMillis();
    @Override public Date getDate() { return new Date(timeMillis); }
    public void add(long offset) { timeMillis += offset; }
  }

  Date getDate();
  long getTimeMillis();
}

// The client of TimeProvider
Order order = new Order().setCreationDate(TimeProvider.get().getDate()));

// In the unit testing
TimeProvider.Mock timeMock = new TimeProvider.Mock();
TimeProvider.instance.setInstance(timeMock);  // Inject mock implementation

InjectableInstanceの実装は非常に簡単です。Javaのリファレンス実装があります。詳細については、ブログ投稿「Injectable Factoryを使用したDependency Indirection」を参照してください。


だから...あなたは依存性注入を可変シングルトンに置き換えますか?それは恐ろしいことに聞こえますが、独立したテストをどのように実行しますか?テストごとに新しいプロセスを開きますか、それとも特定のシーケンスに強制しますか?Javaは私の強力なスーツではありませんが、何か誤解していませんか?
-nvoigt

Java Mavenプロジェクトでは、デフォルトでjunitエンジンが異なるクラスローダーで各テストを実行するため、共有インスタンスは問題になりませんが、同じクラスローダーを再利用する可能性があるため、一部のIDEでこの問題が発生します。この問題を解決するために、クラスは各テストの後に元の状態にリセットするために呼び出されるリセットメソッドを提供します。実際には、異なるテストが異なるモックを使用することはまれな状況です。長年、このパターンを大規模プロジェクトに適用してきました。すべてがスムーズに実行され、移植性とテスト容易性を犠牲にすることなく、読みやすくクリーンなコードを楽しんでいます。
Jianwu陳
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.