web-dev-qa-db-ja.com

この方法でこのコードを記述しているとテスト可能ですが、見当たらない問題がありますか?

IContextというインターフェースがあります。この目的のために、以下のことを除いて、それが何をするかは本当に重要ではありません:

T GetService<T>();

このメソッドが行うことは、アプリケーションの現在のDIコンテナーを調べ、依存関係の解決を試みることです。かなりスタンダードだと思います。

私のASP.NET MVCアプリケーションでは、コンストラクターは次のようになります。

protected MyControllerBase(IContext ctx)
{
    TheContext = ctx;
    SomeService = ctx.GetService<ISomeService>();
    AnotherService = ctx.GetService<IAnotherService>();
}

したがって、各サービスのコンストラクターに複数のパラメーターを追加するのではなく(これは、アプリケーションを拡張する開発者にとって非常に煩わしく時間のかかるものになるため)このメソッドを使用してサービスを取得します。

さて、それは感じが悪いです。しかし、私が現在頭の中でそれを正当化している方法はこれです-私はそれをあざけることができます

できます。コントローラをテストするためにIContextをモックアップすることは難しくありません。私はとにかくしなければならないでしょう:

public class MyMockContext : IContext
{
    public T GetService<T>()
    {
        if (typeof(T) == typeof(ISomeService))
        {
            // return another mock, or concrete etc etc
        }

        // etc etc
    }
}

しかし、私が言ったように、それは間違っていると感じます。どんな考え/虐待も歓迎します。

13

コンストラクターに多くのパラメーターの代わりに1つあることはこの設計の問題のある部分ではありませんIContextクラスが サービスファサード である限り、特にMyControllerBaseで使用される依存関係を提供するためのものであり、コード全体で使用される一般的なサービスロケーターではありません、あなたのコードのその部分は私見で大丈夫です。

最初の例は次のように変更されます

protected MyControllerBase(IContext ctx)
{
    TheContext = ctx;
    SomeService = ctx.GetSomeService();
    AnotherService = ctx.GetAnotherService();
}

これはMyControllerBaseの大幅な設計変更ではありません。このデザインが良いか悪いかは、あなたが望むかどうかという事実にのみ依存します

  • TheContextSomeServiceおよびAnotherServiceが常にallモックオブジェクトで初期化されていること、またはであることを確認してくださいそれらのすべてと実際のオブジェクト
  • または、3つのオブジェクトの異なる組み合わせでそれらを初期化できるようにする(つまり、この場合、パラメーターを個別に渡す必要があります)

したがって、コンストラクターで3の代わりに1つのパラメーターのみを使用することは十分に合理的です。

問題なのは、IContextGetServiceメソッドを公開していることです。私見では、これを回避する必要があります。代わりに、「ファクトリメソッド」を明示的に保持してください。それで、サービスロケータを使用して、私の例のGetSomeServiceおよびGetAnotherServiceメソッドを実装しても問題ありませんか?依存する私見IContextクラスが、サービスオブジェクトの明示的なリストを提供するという特定の目的のための単純な抽象ファクトリーであり続ける限り、これはIMHOで許容されます。抽象ファクトリーは通常、単に「接着剤」コードであり、それ自体を単体テストする必要はありません。それでも、GetSomeServiceのようなメソッドのコンテキストで、サービスロケーターが本当に必要か、明示的なコンストラクター呼び出しが単純ではないか、自問する必要があります。

したがって、IContext実装がパブリックな汎用GetServiceメソッドのラッパーであり、任意のクラスによる任意の依存関係を解決できるようにする設計に固執する場合は、すべてのものが@ベンジャミンホジソンは彼の答えを書いた。

4
Doc Brown

このデザインはService Locator *と呼ばれ、私は好きではありません。それに対する多くの議論があります:

Service Locatorを使用すると、コンテナに接続できます。通常の依存関係注入(コンストラクターが依存関係を明示的に指定する場合)を使用すると、コンテナーを別のコンテナーに直接置き換えるか、new- expressionsに戻ることができます。あなたのIContextでは、それは実際には不可能です。

Service Locatorは依存関係を隠します。クライアントとして、クラスのインスタンスを構築するために必要なものを伝えるのは非常に困難です。何らかのIContextが必要ですが、alsoは、MyControllerBaseを機能させるために、正しいオブジェクトを返すようにコンテキストを設定する必要があります。これは、コンストラクタのシグネチャからはまったく明らかではありません。通常のDIでは、コンパイラーは必要なものを正確に指示します。クラスに多くの依存関係がある場合、あなたはその痛みを感じるはずですリファクタリングを促すので。 Service Locatorは、悪いデザインの問題を隠します。

Service Locatorは実行時エラーを引き起こします。不正な型パラメーターを指定してGetServiceを呼び出すと、例外が発生します。つまり、GetService関数は完全な関数ではありません。 (合計関数はFPの世界からのアイデアですが、基本的には関数が常に値を返す必要があることを意味します。)依存関係が間違っている場合にコンパイラーを助けて通知する方が良い。

Service LocatorはLiskov Substitution Principleに違反しています。その動作はtype引数に基づいて変化するため、Service Locatorは、インターフェース上に無限の数のメソッドを効果的に持っているかのように見ることができます。この引数の詳細は here です。

Service Locatorのテストは困難です。テスト用の偽のIContextの例を示しましたが、これは問題ありませんが、最初にそのコードを記述する必要がない方がよいでしょう。サービスロケーターを経由せずに、偽の依存関係を直接注入するだけです。

要するに、しないでくださいだけです。依存関係の多いクラスの問題に対する魅惑的な解決策のように見えますが、長期的にはあなたの人生を悲惨なものにしてしまいます。

*任意の依存関係を解決できる汎用のResolve<T>メソッドを持つオブジェクトとしてService Locatorを定義しています。これは、(コンポジションルートだけでなく)コードベース全体で使用されます。これは、Service Facade(既知の小さな一連の依存関係をバンドルするオブジェクト)またはAbstract Factory(単一のタイプのインスタンスを作成するオブジェクト)とは異なります-抽象ファクトリのtypeは総称ですがメソッドはそうではありません)。

15

Service Locatorのアンチパターンに対する最良の議論は Mark Seemann によって明確に述べられているので、これがなぜ悪い考えであるのかについては、あまり詳しく説明しません。自分で理解する時間(私もお勧めします Mark's book )。

では、質問に答えましょう-実際の問題を再度述べましょう:

したがって、各サービスのコンストラクターに複数のパラメーターを追加するのではなく(アプリケーションを拡張する開発者にとってこれは本当に面倒で時間がかかるため)、このメソッドを使用してサービスを取得します。

StackOverflow でこの問題に対処する質問があります。そこでのコメントの1つで述べたように:

最高の発言:「コンストラクターインジェクションのすばらしい利点の1つは、単一責任の原則に対する違反が明白に明らかになることです。」

あなたはあなたの問題の解決のために間違った場所を探しています。クラスがあまりにも多くをしているときを知ることは重要です。あなたの場合、私は「ベースコントローラ」の必要がないと強く疑います。実際、OOPでは、ほとんど常に継承の必要はありません。動作と共有機能のバリエーションにより、インターフェイスを適切に使用することで完全に実現されます。これにより、通常、因数分解およびカプセル化されたコードが改善され、スーパークラスコンストラクターに依存関係を渡す必要がなくなります。

私がこれまでに取り組んだすべてのプロジェクトで、ベースコントローラがある場合は、IsUserLoggedIn()GetCurrentUserId()などの便利なプロパティとメソッドを共有する目的でのみ行われました。 [〜#〜]停止[〜#〜]。これは、継承の恐ろしい誤用です。代わりに、これらのメソッドを公開するコンポーネントを作成し、必要な場所でコンポーネントに依存します。このように、コンポーネントはテスト可能なままであり、それらの依存関係は明らかです。

それ以外は別にして、MVCパターンを使用するときは常にskinnyコントローラーをお勧めします。これについて詳しく読むことができます ここ ですが、パターンの本質は単純です。MVCのコントローラーは、MVCフレームワークによって渡された引数を処理し、他の懸念を他のコンポーネントに委譲するという1つのことだけを実行する必要があります。これもまた、単一責任原則です。

より正確な判断を下すためにユースケースを知ることは本当に役立ちますが、正直なところ、十分に因数分解された依存関係よりも基本クラスの方が望ましいシナリオは考えられません。

5
AlexFoxGill