この方法でこのコードを書いているのはテスト可能ですが、見落としているコードに何か問題がありますか?


13

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

T GetService<T>();

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

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

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

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

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

できます。IContextControllerをテストするためにモックアップすることは難しくありません。とにかくする必要があります:

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

        // etc etc
    }
}

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


8
これはサービスロケーターと呼ばれ、私はそれが好きではありません。この件については多くの記事があります- まずはmartinfowler.com/articles/injection.htmlblog.ploeh.dk/2010/02/03/ServiceLocatorisanAnti-Patternをご覧ください。
ベンジャミンホジソン

Martin Fowlerの記事から:「これらの種類のサービスロケーターは、それらを実装に置き換えることができないためテストできないため、悪いことであるという苦情をよく耳にします。この場合、サービスロケーターインスタンスは単なるデータホルダーです。サービスのテスト実装でロケーターを簡単に作成できます。」なぜそれが気に入らないのか説明してもらえますか?たぶん答えに?
LiverpoolsNumber9

8
彼は正しい、これは悪いデザインです。簡単です:public SomeClass(Context c)。このコードは非常に明確ですよね?にthat SomeClass依存していると述べていContextます。エラー、しかし、待って、それはしません!XContextから取得する依存関係のみに依存します。つまり、sを変更しただけでも、変更を加えるたびに破損Contextする可能性がSomeObjectあります。しかし、ええ、あなたはあなたが変更しただけではないことを知っているので、元気です。しかし、良いコードを書くことは、あなたが知っていることではなく、新しい従業員があなたのコードを初めて見たときに知っていることです。ContextYYXSomeClass
ヴァレンテリー

@DocBrown私にとって、それはまさに私が言ったことです-私はここで違いを見ません。さらに説明してもらえますか?
ヴァレンテリー

1
@DocBrown今、あなたの主張がわかります。はい、彼のコンテキストがすべての依存関係のバンドルである場合、これは悪い設計ではありません。しかし、それは悪い命名かもしれませんが、それは単なる仮定でもあります。コンテキストのメソッド(内部オブジェクト)がさらにある場合、OPは明確にする必要があります。また、コードについて議論することは問題ありませんが、これはProgrammers.stackexchangeであるため、私にとっては、OPを改善するために「背後」を確認する必要があります。
ヴァレンテリー

回答:


5

コンストラクターに多数のパラメーターではなく1つのパラメーターを含めることは、この設計の問題のある部分ではありません。長いあなたのようにIContext、クラスは何もなく、されていないサービス・ファサード、specificiallyで使用される依存関係を提供するためMyControllerBaseではなく、あなたの全体のコード全体で使用される一般的なサービスロケータ、あなたのコードのその部分は私見でOKです。

最初の例は、

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

それは実質的なデザイン変更でないでしょうMyControllerBase。この設計が良いか悪いかは、あなたがしたい場合にのみ事実に依存します

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

したがって、コンストラクターで3つではなく1つのパラメーターのみを使用することは完全に合理的です。

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

したがって、IContext実装がパブリックなジェネリックGetServiceメソッドの単なるラッパーであるデザインに固執すると、任意のクラスによる任意の依存関係を解決でき、すべてが@BenjaminHodgsonが彼の答えで書いたものに適用されます。


私はこれに賛同する。例の問題は一般的なGetService方法です。明示的に名前が付けられ型付けされたメソッドへのリファクタリングの方が優れています。それでもIContext、コンストラクターで実装の依存関係を明示的に記述する方が良いでしょう。
ベンジャミンホジソン

1
@BenjaminHodgson:時々改善されるかもしれませんが、常に改善されるとは限りませ 。ctorパラメーターリストがますます長くなると、コードの匂いに注意します。ここに私の元の答えを参照してください。programmers.stackexchange.com/questions/190120/...
ドク・ブラウン

@DocBrownは、コンストラクターのインジェクションの「コードのにおい」が、SRPの違反を示しています。これは実際の問題です。プロパティとして公開するだけで、いくつかのサービスをFacadeクラスにまとめるだけで、問題の原因に対処することはできませ。したがって、ファサードは...他のコンポーネントの周りの単純なラッパーではないはずですが、理想的にそれらを消費する(またはいくつかの他の方法でそれらを簡素化すべきである)によって単純化APIを提供する必要があります
AlexFoxGill

...コンストラクターのインジェクションのような「コードの匂い」は、それ自体では問題ではないことを忘れないでください。問題が特定されると、通常は賢明にリファクタリングすることで解決できるコードに、より深い問題があることを示すヒントに過ぎません
-AlexFoxGill

Microsoftがこれを焼き付けたとき、どこにいましたIValidatableObjectか?
ラバーダック

15

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

Service Locatorはあなたをあなたのコンテナに結び付けます。通常の依存性注入(コンストラクターが依存性を明示的に記述している場合)を使用すると、コンテナーを別のコンテナーに簡単に置き換えるか、new-expressions に戻ることができます。あなたのIContextそれは本当に不可能です。

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

Service Locatorはランタイムエラーを引き起こしますGetService不正な型パラメーターで呼び出すと、例外が発生します。言い換えれば、あなたのGetService関数は総関数ではありません。(合計関数はFPの世界からのアイデアですが、基本的には関数は常に値を返す必要があることを意味します。)依存関係が間違っている場合は、コンパイラーに助けて知らせてください。

サービスロケータは、リスコフ代替原則に違反します。その動作はtype引数に基づいて変化するため、Service Locatorは、インターフェイス上に事実上無限の数のメソッドを持っているように見ることができます!この議論はここで詳細につづられます

Service Locatorはテストが困難です。IContextテスト用のフェイクの例を挙げましたが、これは問題ありませんが、そもそもそのコードを記述する必要はありません。サービスロケーターを経由せずに、偽の依存関係を直接注入するだけです。

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

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


1
サービスロケーターパターン(私は同意します)について苦情を述べています。しかし、実際には、OPの例でMyControllerBaseは、特定のDIコンテナに結合されておらず、これはService Locatorアンチパターンの例でもありません。
ドックブラウン

@DocBrown同意します。それは私の人生を楽にするからではなく、上記の例のほとんどが私のコードに関係しないからです。
LiverpoolsNumber9

2
私にとって、Service Locatorアンチパターンの特徴は一般的なGetService<T>方法です。任意の依存関係を解決することは本当の匂いです。これはOPの例に存在し、正しいものです。
ベンジャミンホジソン

1
Service Locatorの使用に関するもう1つの問題は、柔軟性が低下することです。各サービスインターフェイスの実装は1つしか持てません。IFrobnicatorに依存する2つのクラスを構築し、後で1つは元のDefaultFrobnicator実装を使用する必要があるが、もう1つは実際にCacheingFrobnicatorデコレータを使用する必要があると判断した場合、既存のコードを変更する必要があります依存関係を直接注入するのは、セットアップコード(またはDIフレームワークを使用している場合は構成ファイル)を変更するだけです。したがって、これはOCP違反です。
ジュール

1
@DocBrownこのGetService<T>()メソッドは、任意のクラスを要求することを許可します:「このメソッドは、アプリケーションの現在のDIコンテナを調べ、依存関係を解決しようとします。かなり標準的だと思います。」。この回答の上部にあるあなたのコメントに返信していました。これは100%サービスロケーター
-AlexFoxGill

5

Service Locatorのアンチパターンに対する最良の議論は、Mark Seemannによって明確に述べられているので、なぜこれが悪い考えであるかについてはあまり説明しません-それはあなたが自分で理解するために時間をかけなければならない学習の旅ですマークの本もお勧めします)。

質問に答えるためにOK- 実際の問題を再度説明しましょう:

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

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

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

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

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

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

ユースケースを知ってより正確な判断を下すことは本当に助けになりますが、正直なところ、基本クラスが適切にファクタリングされた依存関係よりも望ましいシナリオは考えられません。


1 -これは他の回答のどれもが本当に対処していない問題について新しいテイクです
ベンジャミン・ホジソン

1
「コンストラクターインジェクションのすばらしい利点の1つは、単一責任原則の違反を明白に明らかにすることです。」本当に良い答え。すべてに同意しないでください。おもしろいことに、私のユースケースでは、100以上のコントローラーを持つシステムでコードを複製する必要はありません(少なくとも)。ただし、SRPについて-注入された各サービスは、それらすべてを使用するコントローラーと同様に、1つの責任を負います。
LiverpoolsNumber9

1
@ LiverpoolsNumber9キーはprotected、新しいコンポーネントへの1行の委任を除いてBaseControllerに何も残らなくなるまで、機能をBaseControllerから依存関係に移動することです。その後、BaseControllerを失い、それらのprotectedメソッドを依存関係への直接呼び出しに置き換えることができます-これにより、モデルが簡素化され、すべての依存関係が明示的になります(100のコントローラーを備えたプロジェクトでは非常に良いことです!)
AlexFoxGill

@ LiverpoolsNumber9-BaseControllerの一部をペーストビンにコピーできれば、具体的な提案をすることができます-AlexFoxGill
1

-2

私は他のすべての人の貢献に基づいてこれに答えを加えています。みんな本当にありがとう。まず、私の答えは次のとおりです。「いいえ、問題はありません」。

Doc Brownの "Service Facade"の答え 私が探していたのは(答えが "いいえ"だった場合)私がやっていることのいくつかの例または拡張であったため、この答えを受け入れました。彼は、A)名前があること、B)おそらくもっと良い方法があることを示唆して、これを提供しました。

Benjamin Hodgsonの「サービスロケーター」の回答 ここで得た知識に感謝する限り、私が持っているのは「サービスロケーター」ではありません。これは「サービスファサード」です。この答えはすべて正しいですが、私の状況ではありません。

USRの回答

これにさらに詳しく取り組みます。

このようにして、多くの静的情報を放棄しています。多くの動的言語のように、決定をランタイムに委ねています。そうすれば、静的検証(安全性)、ドキュメント、およびツールのサポート(オートコンプリート、リファクタリング、使用法の検索、データフロー)を失うことになります。

ツールを失うことはなく、「静的な」タイピングを失うこともありません。サービスファサードは、DIコンテナーで構成したもの、またはを返しますdefault(T)。そして、それが返すのは「型指定」です。反射はカプセル化されます。

コンストラクターの引数として追加のサービスを追加することが大きな負担になる理由はわかりません。

確かに「まれ」ではありません。私はベースコントローラーを使用しているため、コンストラクターを変更する必要があるたびに、10、100、1000個の他のコントローラーを変更する必要があります。

依存性注入フレームワークを使用する場合、パラメーター値を手動で渡す必要さえありません。この場合も、静的な利点をいくつか失いますが、それほど多くはありません。

依存性注入を使用しています。それがポイントです。

そして最後に、ベンジャミンの答えに対するジュールのコメント、 私は柔軟性を失っていません。それが私のサービスファサードです。GetService<T>DIコンテナーを構成するときと同じように、異なる実装を区別したいだけの数のパラメーターを追加できます。したがって、たとえば、私は変更される可能性がGetService<T>()ためにGetService<T>(string extraInfo = null)、この「潜在的な問題」を回避します。


とにかく、みんなに感謝します。それはされています本当に便利。乾杯。


4
ここにサービスファサードがあることに同意しません。このGetService<T>()メソッドは、任意の依存関係を解決(試行)できます。これは、回答の脚注で説明したように、サービスファサードではなく、サービスロケータになります。@DocBrownが示唆するように、それをGetServiceX()/ GetServiceY()メソッドの小さなセットに置き換えると、Facadeになります。
ベンジャミンホジソン

2
Abstract Service Locatorに関するこの記事の最後のセクションを読んで、よく注意してください。これは基本的にあなたがしていることです。私はこのアンチパターンがプロジェクト全体を破壊するのを見てきました-引用に特に注意を払ってください"
AlexFoxGill

3
静的型付けについて-ポイントがありません。DIを使用するクラスに必要なコンストラクターパラメーターを提供しないコードを記述しようとすると、コンパイルされません。これは問題に対する静的なコンパイル時の安全性です。あなたが正しくそれが実際に必要とする引数を提供するように構成されていないIコンテキストを使用してコンストラクタを提供し、あなたのコードを記述する場合、それはコンパイルして実行時に失敗します
ベンアーロンソン

3
@ LiverpoolsNumber9それでは、なぜ強く型付けされた言語を使用しているのでしょうか?コンパイラエラーは、バグに対する最前線の防御です。ユニットテストは2番目です。クラスとその依存関係との間の相互作用であるため、ユニットテストでもピックアップされません。したがって、3番目の防御ラインである統合テストが必要になります。これらを実行する頻度はわかりませんが、IDEがコンパイラエラーに下線を引くのにかかるミリ秒ではなく、数分以上のフィードバックについて話していることになります。
ベンアーロンソン

2
@ LiverpoolsNumber9が間違っています:テストに値を入力しIContextて挿入する必要があり、決定的に重要です:新しい依存関係を追加する場合、実行時にテストが失敗しても、コードはコンパイルされます
-AlexFoxGill
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.