edit
これは私の意見ではなく、誰かの意見を理解しようとしているためです。個人的に私はDIを介してIoCを好む(私の特定の場合 Simple Injector )が、すべての人がこの意見を共有するわけではなく、DIなしで保守可能なコードがどのようになるかについて洞察を得ようとしています。
編集を終了
私はこれについて数週間疑問に思っていましたが、まともな答えを見つけることができないようです。
開発者は、DIフレームワークを使用せずに、SOLID原則に準拠しながら、保守可能なコードをどのように作成できますか?
これをやや抽象化しないサンプルコード:
class ImageProcessor: IImageProcessor
{
public ImageProcessor() { ... }
}
class ImageUploader: IImageUploader
{
public ImageUploader(IImageProcessor processor) { ... }
}
class ImageService
{
public ImageService(IImageUploader uploader) { ... }
}
class ImageController
{
public ImageController()
{
var imageService = new ImageService(new ImageUploader(new ImageProcessor()));
}
}
各クラスはDIを使用して、エンドポイントを除くコンストラクターを介して依存関係を注入します。この場合、すべてを作成するImageController
です。
ImageProcessor
に新しい依存関係が必要になったとします。たとえば、画像の寸法を決定するためにIConfig
を使用します。
class ImageProcessor
{
public ImageProcessor(IConfig config) { ... }
}
class ImageController
{
public ImageController()
{
var imageService = new ImageService(new ImageUploader(new ImageProcessor(new Config())));
}
}
私が目にする主な問題は、すべての複雑さと依存関係がeveryエンドポイント、このImageProcessor
およびeveryコンシューマ(バッチ処理など)。
誰もがそれらについて知っているので、これらの内部依存関係の結合は急上昇するでしょう。
何か不足していますか?
DIを引き続き使用しながら、DIフレームワークなしで保守可能なコードを作成するにはどうすればよいですか?
私が目にする主な問題は、すべての複雑さと依存関係が、すべてのエンドポイント、このImageProcessorを使用するすべてのコントローラー、およびすべてのコンシューマー(バッチ処理など)にまで及ぶことです。
そして、DIフレームワークはどのようにそれを解決しますか?それはあなたが書く必要があるコードの量を減らします、しかしcomplexityはまだそこにあります。まだすべての依存関係があり、すべての結合があります。最上層は下層を使用しており、何かはそれらの作成方法を知っている必要があります。 DIフレームワークを使用すると、それが非表示になり、デバッグが困難になります。
しかし、変更を加える必要がある場合でも、カップリングのなすがままです。
他の人が述べたように、私はその種のコンストラクタチェーンを実行しませんが、一般的なアプローチは問題ありません。クラスはインターフェースを取り、その実装は外部のアクターによって提供されます。コントローラーにそれを認識させることは、複数の責任を強制することになるかもしれませんが、DIフレームワークは、「依存関係の構築方法を知る」責任を通常のコントローラーの作業から分離する1つの方法にすぎません。
それはすべて、依存性注入で達成しようとしていることに依存します。
私が見てきたことから、DIフレームワークの最新の使用法は、コードを単体テストするときに実際の実装をスタブ/モックと交換できるようにすることです。
DIフレームワークを使用したくないが、テスト中のクラスの依存関係を模擬するためのDIの利点を得たい場合は、デフォルト値を使用できます。の代わりに:
class ImageUploader
{
public ImageUploader(IImageProcessor processor) { ... }
}
あなたが持っているでしょう:
class ImageUploader
{
public ImageUploader(IImageProcessor processor = null)
{
this.processor = processor || new ImageProcessor();
}
}
このように、アプリケーションコードでは、コンストラクタに何も渡す必要がありません。デフォルトの依存関係が使用されます。ただし、単体テストを行う場合は、コンストラクターを使用して必要なスタブとモックを挿入します。
明らかに、これはisインターフェイスのデフォルト実装がある状況でのみ機能します。デフォルトがない場合(アプリがアプリケーション構成に基づいてPostgreSqlProvider
とOracleProvider
のどちらかを選択する必要がある場合など)、オブジェクトをコンストラクターに渡す必要があります。
場合によっては、チェーン内のほぼすべての場所にデフォルトがあるにもかかわらず、どこかに、カスタム依存関係を必要とするクラスがあるという状況が発生することがあります。たとえば、ImageProcessor
には、InMemoryStorage
またはFileStorage
のいずれかのストレージメカニズムが必要な場合があります。この場合:
依存関係をチェーンを介して渡すことができるので、例ではコントローラーに属して選択を行います。欠点は、質問のイラストとして使用したのと同じチェーンになってしまうことです。
ストレージメカニズムのインスタンスを格納するシングルトンを使用して、チェーンを解放し、それを必要とするクラス(この場合はImageProcessor
)のみがアクセスできるようにします。 反対することをお勧めしますImageProcessor
をテストすることを困難にすることは不可能にするためです。
または、対応するインスタンスとの依存関係を必要とするクラスに供給するApplicationConfiguration
などのオブジェクトがあります。あなたの場合、それは持っていることを意味します:
interface IApplicationConfiguration
{
IStorage getImageStorage();
}
class ApplicationConfiguration: IApplicationConfiguration
{
IStorage getImageStorage()
{
... // Based on a config file, return either `InMemoryStorage` or
//`FileStorage`.
}
}
class ImageProcessor: IImageProcessor
{
... // All logic which should be unit tested goes here.
}
class DefaultImageProcessor: ImageProcessor
{
// Contains no business logic; simply uses
// `ApplicationConfiguration.getImageStorage` to retrieve the instance
// of the storage.
}
この3番目のソリューションには、すべてのコードを単体テストできるという利点があります。ただし、DefaultImageProcessor
は、基本ImageProcessor
クラスに適切な依存関係を渡すことに制限されています。
これは、実装とスタブ/モック間の依存関係を交換できるようにするために機能しますが、これがこのアプローチのほとんど唯一の利点であり、依存関係注入の他のすべての利点は失われます。
欠点
考慮する必要がある欠点の1つは、パッケージ/アセンブリのレベルで、DIが使用されなかった場合とまったく同じ順序でクラスとその依存関係をリンクすることです。
たとえば、DIを使用して、ImageService
とIImageUploader
を含むアセンブリ/パッケージを作成できます。このアセンブリ/パッケージはImageUploader
について何も知りません。これの利点(そしてこれが依存性注入の最も重要な利点の1つです)は、IImageUploader
の実装の1つを含むアセンブリ/パッケージを、含まれるアセンブリ/パッケージにまったく影響を与えることなく変更できることですImageService
。再構築する必要はありません。新しいバージョンのパッケージをパッケージリポジトリに公開する必要はありません。 ImageService
変更されていませんなので、これは完全に理にかなっています。
コンストラクターのデフォルトのパラメーターでは、この利点は無駄になります。 ImageService
toImageUploader
を含むパッケージからのアセンブリ/パッケージレベルの依存関係があるため、他に選択肢はありません。ここで、ImageUploader
の実装で何かを変更すると、ImageService
を含むパッケージも再コンパイルする必要があります。
言い換えると、デフォルトのパラメーターアプローチがDIのように見える場合、反転がないため、IoCではありません(したがって、DIは単なるIoCの形式であるため、DIではありません)。
それは重要ですか?
テスト中に依存関係を入れ替えることが唯一の目標である場合、それはそれほど重要ではありません。
目標がクリーンなアーキテクチャであることである場合、デフォルトのパラメータは機能しません。
IoCの場合、いずれにしてもインスタンスをコンストラクターに渡すことになります。自分でコードを記述しないようにする唯一の方法は、ジョブをライブラリに委任することです。これがまさにDIフレームワークが行うことです。その目的は、関係を単純化したり、そのようなものにすることではありません。 DIフレームワークの目標は、DIに関連するコードをユーザーから隠すことです(通常、Reflectionを使用することで、独自の問題が発生します)。
もちろん、さまざまなレベルでさまざまなアプローチを自由に使用できます。たとえば、ImageUploader
とImageProcessor
の間に強い関係があることが完全に理にかなっていると判断する場合があります。彼らは同じアセンブリ/パッケージ内で並んでいることに気づき、この場合、コンストラクターでデフォルトのパラメーターを使用するのが賢明な選択です。
同時に、ImageProcessor
がアプリケーションの構成に依存するストレージメカニズムに依存するようにしたい場合があります。この場合、上記で説明したDefaultImageProcessor
を使用した解決策が機能する可能性があります。
そして、アプリケーション内の任意のクラスからアクセスしたいログクラスのケースがあるかもしれません。ここで、単体テストのために実際のロガーをスタブに置き換えることができるとすぐに、静的ファクトリーがうまく機能する可能性があります。
最後に、ImageService
はImageUploader
およびImageProcessor
から完全に独立していることがわかります。別のパッケージに入れて、IoCを使用します。 ImageController
は、サービスの初期化時にIImageUploader
のインスタンスを渡します。
最初に注意すべきことは、フレームワークを使用する依存性注入(DI)とDIを混同していることです。どちらもDIです。前者は、サードパーティのライブラリ/フレームワークを使用してインジェクションを実現していないという事実を強調するために、「純粋なDI」と呼ばれることがよくあります。すべてユーザーのコードで処理されます。
コード例を見てみましょう:
var imageService = new ImageService(new ImageUploader(new ImageProcessor(new Config())));
これはDIの動作です。ImageProcessor
はIConfig
に依存しているため、コンストラクターを介して提供します。 ImageUploader
はIImageUploader
に依存しています。したがって、その依存関係を提供します。 ImageUploader
は、必要がないため、Config
を認識していません。 DIを使用することで、これら2つを切り離しました。
このコード例では、純粋なDIを使用しています。依存関係を手動で相互にマッピングしています。フレームワークが行うことはすべて、物事がどのようにマッピングされるかについてのルールを記述させ、それから上記のコンストラクタチェーンを生成します。
私が目にする主な問題は、すべての複雑さと依存関係がすべてのエンドポイントに向かって泡立つことです
エンドポイントは1つだけです。したがって、クラスImageController
は次のようになります。
class ImageController
{
public ImageController(IImageService imageService)
{
...
}
}
そして、アプリケーションのルートはIImageService
実装の依存関係を解決し、それを作成してImageController
に注入する責任があります。
コードの一部は互いに依存している必要があります。 DIの目的は、これらの依存関係を抽象化し、実行時にそれらを解決することです。はい、これは複雑さを生み出します。すべての抽象化は複雑さを生み出します。これは、2つの間のトレードオフです。
あなたのコメントは、DIフレームワークで実際にDIを使用しているのではないかと思います。私はあなたを誤解しているだけかもしれませんが、あなたの現在の ``の実装は次のように見えると思います:
class ImageController
{
public ImageController()
{
var imageService = DIContainer.GetInstance<IImageService>();
}
}
その場合は、DIを使用していません。コンテナーをサービスロケーターとして使用しています。これには、複雑さを大幅に軽減するという利点があります。しかし、たとえば、Config
がImageService
のコピーを取得するのを止めることができないため、コードは結合されたスパゲッティの塊になります。
前に言ったように、それはトレードオフについてのすべてです。 DIは複雑さを追加します。ただし、コードの分離に役立ち、テストと保守が容易になります。毒を選んでください...
TL; DR;
依存関係のインジェクションとしてブランド化されているものFrameworkには、プログラマーとしてどのように機能させるかがわかりません。
開発者は、DIフレームワークを使用せずに、SOLID原則に準拠しながら、保守可能なコードをどのように作成できますか?
依存性注入を実際に使用し、型の依存関係がその型にどのような依存関係があるかを明確に型付けする。保守可能なSOLIDコードを作成するために、フレームワークは必要ありません。
依存性注入が何を意味するか、または表すかについて奇妙な認識があるようですので、私はこれを述べたいと思います:
Dependency Injectionsは、クラスが要求するすべてのアクションを実行するためにクラスにどのような依存関係があるかを示唆するタイプを意味します。
本当に、これですべてです。もちろん、これはアーキテクチャの作成という点で多くのことを包含し、テストに大きな影響を与えますが、依存関係の注入は実際にはそれだけです:依存関係カスタムタイプをインスタンス化する必要があります。
依存関係注入コンテナが存在します。これは主に、プログラマとして、型の一部をリンクする方法、および型が解決される方法を定義します。これらのコンテナーを使用すると、インターフェースの解決方法を決定する方法、このインターフェースまたはこのインターフェースまたはそのタイプのインターフェースに使用する具体的なタイプを決定できますが、それだけがコンテナーです。これらは、関連するアプリケーションを作成するために、実際のフレームワーク(ユーザーまたは他の誰かが作成したもの)で使用する必要があります。
これらのコンテナーは、構成ファイルやリフレクションを使用して、タイプを検査し、必要なものを調べ、インスタンス化してインスタンスにフィードします(うまくいけば、全員で共有する必要のあるインスタンスをキャッシュします)。そして、最も純粋に使用可能な形式で、依存性注入があります。
コントローラーの作成または構成をさらに委任する方法があるかもしれません。多くのフレームワークでは、そのためのファクトリを提供できます。
一部のフレームワークが引数なしのコンストラクターを使用してインスタンスを作成するポイントに確実に到達した場合でも、手動でファクトリーを呼び出して、必要なdirect(!)依存関係を提供できます。
いずれの場合も、ファクトリはアプリの起動時に構成された構成を使用できます。このようにして、依存関係の配線の複雑さは1箇所にとどまります。
TLDR:DRYの原則に違反しているため、作成ロジックを中央のファクトリーに集中化し、必要に応じて再利用します。