サーチ…
備考
依存性注入によって解決される問題
依存関係注入を使用しなかった場合、 Greeter
クラスは次のようになります。
public class ControlFreakGreeter
{
public void Greet()
{
var greetingProvider = new SqlGreetingProvider(
ConfigurationManager.ConnectionStrings["myConnectionString"].ConnectionString);
var greeting = greetingProvider.GetGreeting();
Console.WriteLine(greeting);
}
}
それは、挨拶を提供するクラスの作成を制御し、SQL接続文字列がどこから来ているかを制御し、出力を制御するので、「コントロール・フリーク」です。
依存関係インジェクションを使用して、 Greeter
クラスは責任を放棄して単一の責任を与え、提供された挨拶を書いています。
Dependency Inversion Principleでは、クラスは他の具体的なクラスではなく抽象(インターフェイスなど)に依存する必要があることを示しています。クラス間の直接の依存(結合)は、保守を徐々に困難にする可能性があります。抽象度に応じて、その結合を減少させることができる。
依存性注入は、抽象化に依存するクラスの作成につながるため、依存性の逆転を達成するのに役立ちます。 Greeter
クラスは、 IGreetingProvider
とIGreetingWriter
実装の詳細については何も知らない。注入された依存関係がそれらのインタフェースを実装していることだけがわかります。つまり、 IGreetingProvider
とIGreetingWriter
を実装する具体的なクラスを変更しても、 Greeter
は影響しません。どちらも、それらをまったく異なる実装に置き換えません。インタフェースに対する変更のみが行われます。 Greeter
は分離されています。
ControlFreakGreeter
はControlFreakGreeter
を正しくControlFreakGreeter
ができません。小さなコード単位をテストしたいが、テストにはSQLへの接続とストアドプロシージャの実行が含まれる。コンソール出力のテストも含まれます。 ControlFreakGreeterはそれほど多くのことをするので、他のクラスとは独立してテストすることは不可能です。
Greeter
は単体テストが容易です。なぜなら、ストアドプロシージャを呼び出すか、コンソールの出力を読み取るよりも、実行と検証が容易な依存関係の実装を実装することができるからです。 app.configに接続文字列は必要ありません。
IGreetingProvider
とIGreetingWriter
の具体的な実装はもっと複雑になるかもしれません。彼らは自分自身の依存関係を持っているかもしれません。 (たとえば、SQL接続文字列をSqlGreetingProvider
ます)。しかし、その複雑さは、インターフェイスにのみ依存する他のクラスからは「隠されています」。これにより、他のクラスに対応する変更を加える必要がある「波及効果」なしに、あるクラスを変更することが容易になります。
依存性注入 - 簡単な例
このクラスはGreeter
と呼ばれます。その役割は、挨拶を出力することです。それには2つの依存関係があります。それには出力する挨拶を与える何かが必要です。そして、挨拶を出力する方法が必要です。これらの依存関係は、どちらもインタフェース、 IGreetingProvider
、 IGreetingWriter
として記述されています。この例では、これら2つの依存関係がGreeter
「注入」されています。 (この例に続く詳細説明)
public class Greeter
{
private readonly IGreetingProvider _greetingProvider;
private readonly IGreetingWriter _greetingWriter;
public Greeter(IGreetingProvider greetingProvider, IGreetingWriter greetingWriter)
{
_greetingProvider = greetingProvider;
_greetingWriter = greetingWriter;
}
public void Greet()
{
var greeting = _greetingProvider.GetGreeting();
_greetingWriter.WriteGreeting(greeting);
}
}
public interface IGreetingProvider
{
string GetGreeting();
}
public interface IGreetingWriter
{
void WriteGreeting(string greeting);
}
Greeting
クラスはIGreetingProvider
とIGreetingWriter
両方に依存しますが、いずれかのインスタンスを作成する責任はありません。代わりに、コンストラクタでそれらを必要とします。 Greeting
インスタンスを作成するものは、その2つの依存関係を提供する必要があります。依存関係を「注入」することができます。
依存関係はコンストラクタのクラスに提供されるため、これはコンストラクタインジェクションとも呼ばれます。
いくつかの一般的な規則:
- コンストラクタは、依存関係を
private
フィールドとして保存します。クラスがインスタンス化されるとすぐに、それらの依存関係はクラスの他のすべての非静的メソッドで使用できます。 -
private
フィールドはreadonly
です。それらがコンストラクタに設定されると、変更することはできません。これは、これらのフィールドがコンストラクタの外部で変更できないようにする(できない)ことを示します。これにより、クラスの存続期間中にこれらの依存関係が利用できるようになります。 - 依存関係はインタフェースです。これは厳密には必要ではありませんが、依存関係の1つの実装を別の実装と簡単に置き換えることができるため、一般的です。また、単体テストの目的でインターフェイスの模擬バージョンを提供することもできます。
依存性注入が単体テストをより簡単にする方法
これは、 IGreetingProvider
とIGreetingWriter
2つの依存関係を持つGreeter
クラスの前の例をベースにしています。
IGreetingProvider
の実際の実装では、API呼び出しまたはデータベースから文字列を取得できます。 IGreetingWriter
の実装によって、コンソールに挨拶が表示されることがあります。しかし、 Greeter
はそのコンストラクタに依存性が注入されているため、これらのインタフェースの模擬バージョンを注入する単体テストを書くのは簡単です。実生活では、 Moqのようなフレームワークを使うかもしれませんが、この場合、私はそれらの模擬実装を書くでしょう。
public class TestGreetingProvider : IGreetingProvider
{
public const string TestGreeting = "Hello!";
public string GetGreeting()
{
return TestGreeting;
}
}
public class TestGreetingWriter : List<string>, IGreetingWriter
{
public void WriteGreeting(string greeting)
{
Add(greeting);
}
}
[TestClass]
public class GreeterTests
{
[TestMethod]
public void Greeter_WritesGreeting()
{
var greetingProvider = new TestGreetingProvider();
var greetingWriter = new TestGreetingWriter();
var greeter = new Greeter(greetingProvider, greetingWriter);
greeter.Greet();
Assert.AreEqual(greetingWriter[0], TestGreetingProvider.TestGreeting);
}
}
IGreetingProvider
とIGreetingWriter
の動作はこのテストとは関係ありません。私たちはGreeter
が挨拶をしてそれを書くことをテストしたいと思っています。 Greeter
の設計(依存性注入を使用)により、複雑な可動部品を使用せずに模擬依存関係を注入することができます。私たちがテストしていることは、 Greeter
がそれらの依存関係を想定しているからです。
依存性注入コンテナ(IoCコンテナ)を使用する理由
依存性注入とは、依存関係を制御しないようにクラスを作成することです。その代わりに、依存関係が提供されます(注入されます)
Castle Windsor、Autofac、SimpleInjector、Ninject、Unityなどの依存性注入フレームワーク(しばしば "DIコンテナ"、 "IoCコンテナ"、または単に "コンテナ"と呼ばれる)を使用するのと同じことではありません。
コンテナは単に依存性注入を容易にします。たとえば、依存関係注入に依存するいくつかのクラスを作成するとします。あるクラスはいくつかのインターフェースに依存し、それらのインターフェースを実装するクラスは他のインターフェースに依存します。いくつかは特定の値に依存します。そして楽しみのために、これらのクラスのいくつかはIDisposable
を実装し、処分する必要があります。
それぞれのクラスはよく書かれており、簡単にテストできます。しかし今は別の問題があります:クラスのインスタンスを作成することはずっと複雑になっています。 CustomerService
クラスのインスタンスを作成しているとします。それには依存関係があり、依存関係には依存関係があります。インスタンスの構築は次のようになります。
public CustomerData GetCustomerData(string customerNumber)
{
var customerApiEndpoint = ConfigurationManager.AppSettings["customerApi:customerApiEndpoint"];
var logFilePath = ConfigurationManager.AppSettings["logwriter:logFilePath"];
var authConnectionString = ConfigurationManager.ConnectionStrings["authorization"].ConnectionString;
using(var logWriter = new LogWriter(logFilePath ))
{
using(var customerApiClient = new CustomerApiClient(customerApiEndpoint))
{
var customerService = new CustomerService(
new SqlAuthorizationRepository(authorizationConnectionString, logWriter),
new CustomerDataRepository(customerApiClient, logWriter),
logWriter
);
// All this just to create an instance of CustomerService!
return customerService.GetCustomerData(string customerNumber);
}
}
}
巨大な構造全体をCustomerService
返す別の関数に入れてみてはどうでしょうか? 1つの理由は、各クラスの依存関係がそれに注入されるため、クラスはその依存関係がIDisposable
かどうかを知る責任がないということです。それは単にそれらを使用します。そのため、完全に構築されたCustomerService
を返すGetCustomerService()
関数がある場合、そのクラスには使い捨て可能なリソースが数多く含まれており、アクセスまたは処分する方法はありません。
そして、 IDisposable
を処分するのとは別に、そのような入れ子になったコンストラクタを呼びたい人はいますか?これは簡単な例です。それははるかに悪化する可能性があります。繰り返しますが、それは私たちが間違った方法でクラスを書いたことを意味するものではありません。クラスは個別に完璧かもしれません。課題はそれらを一緒に構成することです。
依存性注入コンテナはそれを単純化します。これにより、各依存関係を満たすために使用するクラスまたは値を指定することができます。このわずかに単純化された例は、Castle Windsorを使用しています。
var container = new WindsorContainer()
container.Register(
Component.For<CustomerService>(),
Component.For<ILogWriter, LogWriter>()
.DependsOn(Dependency.OnAppSettingsValue("logFilePath", "logWriter:logFilePath")),
Component.For<IAuthorizationRepository, SqlAuthorizationRepository>()
.DependsOn(Dependency.OnValue(connectionString, ConfigurationManager.ConnectionStrings["authorization"].ConnectionString)),
Component.For<ICustomerDataProvider, CustomerApiClient>()
.DependsOn(Dependency.OnAppSettingsValue("apiEndpoint", "customerApi:customerApiEndpoint"))
);
これを「依存関係の登録」または「コンテナの設定」と呼びます。翻訳済み、これはWindsorContainer
伝えます:
- クラスに
ILogWriter
が必要な場合は、LogWriter
インスタンスを作成します。LogWriter
はファイルパスが必要です。AppSettings
からこの値を使用します。 - クラスに
IAuthorizationRepository
が必要な場合は、SqlAuthorizationRepository
インスタンスを作成します。接続文字列が必要です。ConnectionStrings
セクションからこの値を使用します。 - クラスに
ICustomerDataProvider
が必要な場合は、CustomerApiClient
を作成し、必要な文字列をAppSettings
から提供します。
コンテナから依存関係を要求するときは、それを依存関係を「解決する」と呼びます。コンテナを使って直接行うのは悪い習慣ですが、それは別の話です。デモンストレーションの目的で、これを行うことができます:
var customerService = container.Resolve<CustomerService>();
var data = customerService.GetCustomerData(customerNumber);
container.Release(customerService);
コンテナは、 CustomerService
がIAuthorizationRepository
およびICustomerDataProvider
依存していることをICustomerDataProvider
ます。これらの要件を満たすためにどのクラスを作成する必要があるかはわかっています。これらのクラスには依存関係が多く、コンテナにはそれらを満たす方法があります。それは、 CustomerService
インスタンスを返すまで、必要なすべてのクラスを作成します。
IDoesSomethingElse
ようにIDoesSomethingElse
に登録されていない依存関係が必要な場合は、 CustomerService
を解決しようとすると、その要件を満たすために何も登録していないことを明示する明確な例外がスローされます。
各DIフレームワークの動作は少し異なりますが、通常は、特定のクラスがどのようにインスタンス化されるかを制御します。たとえば、 LogWriter
インスタンスを1つ作成し、それをILogWriter
に依存するすべてのクラスに提供したいのですか、毎回新しいクラスを作成する必要がありますか?ほとんどのコンテナには、それを指定する方法があります。
IDisposable
を実装するクラスはどうですか?だから我々はcontainer.Release(customerService);
を呼び出しcontainer.Release(customerService);
最後に。 (ウィンザー含む)ほとんどのコンテナが作成された依存関係のすべてを一歩となりDispose
処分必要なものを。 CustomerService
がIDisposable
場合、それも処分されます。
上記のように依存関係を登録すると、より多くのコードを記述することができます。しかし、多くの依存関係を持つクラスがたくさんあるときは、それは本当に報われます。また、依存性注入を使わずに同じクラスを書く必要がある場合、クラスが多い同じアプリケーションでは保守やテストが難しくなります。
これはなぜ依存性注入コンテナを使うのかを表面に傷つける。アプリケーションを構成する方法 (そして正しく使用する方法)は単なる1つのトピックではなく、指示と例がコンテナごとに異なるため、多くのトピックです。