5

依存関係の逆転を取得しようとしている、または少なくともそれを適用する方法を理解しようとしていますが、現時点で私が抱えている問題は、蔓延している依存関係をどのように処理するかです。この典型的な例はトレース ロギングですが、私のアプリケーションには、すべてではないにしてもほとんどのコードが依存する多くのサービスがあります (トレース ロギング、文字列操作、ユーザー メッセージ ロギングなど)。

これに対する解決策はどれも特に口に合うようには見えません。

  • コンストラクター依存性注入を使用すると、ほとんどのクラスが明示的にそれらの依存性を必要とするため (構築するオブジェクトにそれらを渡すだけではない)、ほとんどのコンストラクターに複数の、多くの標準的な依存性が注入されることになります。
  • サービスロケーターパターンは、依存関係を地下に追いやり、コンストラクターから削除しますが、依存関係が必要であることを明示しないように非表示にします
  • シングルトン サービスはシングルトンであり、依存関係を隠す役割も果たします。
  • これらすべての共通サービスを 1 つの CommonServices インターフェイスにまとめ、それを注入することは、a) デメテルの法則に違反し、b) 一般的なものではなく特定のものではありますが、実際には Service Locator の別の名前にすぎません。

これらの種類の依存関係を構築する方法、または実際に上記のソリューションのいずれかの経験について、他の提案はありますか?

私は特定の DI フレームワークを念頭に置いていないことに注意してください。実際、私たちは C++ でプログラミングしており、手動で注入を行っています (実際に依存関係が注入されている場合)。

4

3 に答える 3

2

サービスロケーターパターンは依存関係を地下に追いやるだけで、シングルトンサービスはシングルトンであり、依存関係を隠す役割も果たします

これは良い観察です。依存関係を非表示にしても、それらは削除されません。代わりに、クラスが必要とする依存関係の数に対処する必要があります。

コンストラクター依存性注入を使用すると、ほとんどのクラスが明示的にそれらの依存性を必要とするため、ほとんどのコンストラクターにいくつかの、多くの、標準的に注入された依存性があることを意味します。

この場合、単一責任の原則に違反している可能性があります。言い換えれば、それらのクラスはおそらく大きすぎて、やりすぎです。ロギングとトレースについて話しているので、ロギングが多すぎないか自問する必要があります。しかし、一般的に、ロギングとトレースは分野横断的な問題であり、システム内の多くのクラスに追加する必要はありません。SOLIDの原則を正しく適用すると、この問題は解消されます (ここで説明されているように)。

于 2012-10-25T14:25:45.093 に答える
2
class Base {
 public:
  void doX() {
    doA();
    doB();
  }

  virtual void doA() {/*does A*/}
  virtual void doB() {/*does B*/}
};

class LoggedBase public : Base {
 public:
  LoggedBase(Logger& logger) : l(logger) {}
  virtual void doA() {l.log("start A"); Base::doA(); l.log("Stop A");}
  virtual void doB() {l.log("start B"); Base::doB(); l.log("Stop B");}
 private:
  Logger& l;
};

これで、ロガーを認識している抽象ファクトリを使用して LoggedBase を作成できます。他の誰もロガーについて知る必要はなく、LoggedBase について知る必要もありません。

class BaseFactory {
 public:
  virtual Base& makeBase() = 0;
};

class BaseFactoryImp public : BaseFactory {
 public:
  BaseFactoryImp(Logger& logger) : l(logger) {}
  virtual Base& makeBase() {return *(new LoggedBase(l));}
};

ファクトリの実装は、グローバル変数に保持されます。

BaseFactory* baseFactory;

また、「main」または main に近い関数によって BaseFactoryImp のインスタンスに初期化されます。その関数だけが BaseFactoryImp と LoggedBase について知っています。他の誰もがそれらすべてを知らずに幸せです。

于 2013-05-22T16:57:06.387 に答える
2

依存性逆転の原則は SOLID 原則の一部であり、特に高レベルのアルゴリズムのテスト容易性と再利用を促進するための重要な原則です。

背景: Uncle Bob の Web ページに示されているように、Dependency Inversion は具象ではなく抽象化に依存するものです。

実際には、クラスが別のクラスを直接インスタンス化するいくつかの場所を変更して、内部クラスの実装を呼び出し元が指定できるようにする必要があります。

たとえば、Model クラスがある場合、特定のデータベース クラスを使用するようにハード コードするべきではありません。その場合、Model クラスを使用して別のデータベース実装を使用することはできません。これは、別のデータベース プロバイダーを使用している場合や、テスト目的でデータベース プロバイダーを偽のデータベースに置き換えたい場合に役立ちます。

Model が Database クラスで「新規」を実行するのではなく、Database クラスが実装する IDatabase インターフェイスを使用するだけです。モデルは具体的なデータベース クラスを参照することはありません。しかし、データベース クラスをインスタンス化するのは誰でしょうか? 1 つの解決策は、コンストラクター インジェクション (依存性インジェクションの一部) です。この例では、Model クラスには、それ自体をインスタンス化するのではなく、使用する IDatabase インスタンスを取る新しいコンストラクターが与えられます。

これにより、モデルの元の問題が解決され、具体的なデータベース クラスが参照されなくなり、IDatabase 抽象化を通じてデータベースが使用されます。しかし、それはデメテルの法則に反するという、質問で言及された問題をもたらします。つまり、この場合、Model の呼び出し元は、以前は認識していなかった IDatabase について知る必要があります。モデルは現在、その仕事をどのように遂行するかについての詳細をクライアントに公開しています。

これで大丈夫だとしても、一部のトレーナーを含む多くの人々を混乱させるように見える別の問題があります. モデルなどのクラスが別のクラスを具体的にインスタンス化するときはいつでも、依存関係の逆転の原則に違反しているため、それは悪いことであるという前提があります。しかし、実際には、このような厳格なルールに従うことはできません。具象クラスを使用する必要がある場合があります。たとえば、例外をスローする場合は、「新しいものにする」必要があります (たとえば、新しい BadArgumentException(...) をスローしました)。または、文字列、辞書などの基本システムのクラスを使用します。

すべてのケースで機能する単純なルールはありません。達成しようとしていることが何であるかを理解する必要があります。テスト容易性を追求している場合、Model クラスが Database クラスを直接参照すること自体は問題ではありません。問題は、Model クラスには別の Database クラスを使用する他の手段がないという事実です。IDatabase を使用するように Model クラスを実装し、クライアントが IDatabase 実装を指定できるようにすることで、この問題を解決します。クライアントによって指定されていない場合、モデルは具体的な実装を使用できます。

これは、C++ 標準ライブラリを含む多くのライブラリの設計に似ています。たとえば、宣言 std::set コンテナーを見ると、次のようになります。

template < class T,                        // set::key_type/value_type
           class Compare = less<T>,        // set::key_compare/value_compare
           class Alloc = allocator<T> >    // set::allocator_type
           > class set;

コンペアラーとアロケーターを指定できることがわかりますが、ほとんどの場合、デフォルト、特にアロケーターを使用します。STL には、特にローカリゼーション、エンディアンネス、ロケールなどのためにストリーミングの詳細な側面を拡張できる IO ライブラリに、そのような多くのファセットがあります。

テスト容易性に加えて、これにより、アルゴリズムが内部で使用するクラスの完全に異なる実装を使用して、上位レベルのアルゴリズムを再利用できます。

そして最後に、依存関係を逆転させたくないシナリオに関して以前に行った主張に戻ります。つまり、例外クラス BadArgumentException をインスタンス化する場合など、具象クラスをインスタンス化する必要がある場合があります。ただし、テスト容易性を追求している場合は、実際にはこれの依存関係を逆にしたいと主張することもできます。例外のすべてのインスタンス化がクラスに委任され、抽象インターフェイスを介して呼び出されるように Model クラスを設計することができます。そうすれば、Model クラスをテストするコードは、テストで使用状況を監視できる独自の例外クラスを提供できます。

「getsystemtime」などのシステム コールのインスタンス化を単純に抽象化して、単体テストを通じて夏時間とタイム ゾーンのシナリオをテストできるようにする例を同僚に教えてもらいました。

YAGNI の原則に従います。抽象化が必要かもしれないという理由だけで抽象化を追加しないでください。テスト ファーストの開発を実践している場合、適切な抽象化が明らかになり、テストに合格するのに必要なだけの抽象化が実装されます。

于 2013-05-17T18:33:30.177 に答える