こんにちは、トイロジックでツール業務を担当しているプログラマーのIです。主にプロジェクトで使用するGUI/CUIツールやDCCツールの作成・整備を行っています。
本記事ではツールを作成する上で知っておくと便利な「DIコンテナ」というフレームワークについてご紹介できればと思います。
DIコンテナって何?
「DIコンテナ」という言葉を聞いたことが無い方もいらっしゃると思いますので、簡単に説明させていただきます。
DIコンテナとはDI(Dependency Injection)というパターンを実現するためのフレームワークです。
そもそも「DI」って何?という方もいらっしゃると思います。DIとは依存性の注入、オブジェクトが依存する他のオブジェクトを受け取るためのデザインパターンです。
DIパターンでは、あるオブジェクトが他のサービスを利用する際に、そのサービスを構築する方法を知る必要がありません。代わりに、受け取り側は「インジェクタ」
と呼ばれる外部コードから依存関係を提供されます。
簡単に言えば、クラスから依存性を取り除こう!というものです。これを読んでもあまりピンと来ないかと思いますので、次に使用例を上げてみようと思います。
DIコンテナを使ってみる前に
使用例を上げる前に、DIコンテナといっても複数の種類が存在します。今回は弊社でも使用している「MEF (Managed Extensibility Framework)」を使用して説明していきます。
MEFはMicrosoftによって作られたDIコンテナで、Visual Studioにも使用されています。Visual Studioには、MEFコンポーネントパーツとして拡張できる拡張ポイントが提供されており、これによって外からの拡張が可能となっています。
MEF以外にも様々なDIコンテナが存在するので、興味のある方はぜひ調べてみてください!
DIコンテナを使ってみよう!
今回は以下のような場合を想定してみます。(あくまで例です!)
- 特定のサービスがログを出力する
- ログはコンソール上に出力するかもしれないし、ファイルとして出力するかもしれない
- なるべく複数のコードを書き換えないといけない状態を避けたい
では、
- DIパターンを使用しない場合
- DIパターンを使用する場合
- DIコンテナを使用する場合
でそれぞれコードを見てみましょう。
DIパターンを使用しない場合
DIパターンを使用しない場合のコードです。
namespace NotDIPatternSample
{
public class Program
{
static void Main(string[] args)
{
// OutputService のインスタンスを生成します.
var outputService = new OutputService();
outputService.Write();
}
}
}
using System;
namespace NotDIPatternSample
{
/// <summary>
/// ログをコンソール上に表示するためのサービス
/// </summary>
public class ConsoleLogger
{
/// <summary>
/// メッセージをロガーに送ります
/// </summary>
/// <param name="message">メッセージ文</param>
public void Write(string message)
{
Console.WriteLine(message);
}
}
}
namespace NotDIPatternSample
{
/// <summary>
/// 何かしらのログを出力するためのサービス
/// </summary>
public class OutputService
{
private readonly ConsoleLogger _logger;
/// <summary>
/// コンストラクタ
/// </summary>
public OutputService()
{
// クラス内部でロガーのインスタンスを生成します.
// これにより、ConsoleLogger に直接依存することになります.
_logger = new ConsoleLogger();
}
/// <summary>
/// ログの書き込みを行います
/// </summary>
public void Write()
{
_logger.Write("ログを出力するよ!");
}
}
}
こちらの例では OutputService
が ConsoleLogger
に直接依存してしまっていますね。もしロガーを別のものに変えたい場合、OutputService
も変更する必要が出てきます。
DIパターンを使用する場合
次にDIパターンを使用する場合のコードです。
namespace DIPatternSample
{
public class Program
{
static void Main(string[] args)
{
// OutputService へ受け渡すためのロガーのインスタンスを生成します.
var logger = new ConsoleLogger();
// OutputService のインスタンスを生成します.
// 外からOutputServiceのコンストラクタの引数に渡すためのロガーの
// インスタンスを生成し、渡してあげます.
// OutputService はロガーの実体を知る必要はありません.
var outputService = new OutputService(logger);
outputService.Write();
}
}
}
using System;
namespace DIPatternSample
{
/// <summary>
/// ログをコンソール上に表示するためのサービス
/// </summary>
public class ConsoleLogger : ILogger
{
/// <summary>
/// メッセージをロガーに送ります
/// </summary>
/// <param name="message">メッセージ文</param>
public void Write(string message)
{
Console.WriteLine(message);
}
}
}
using System;
namespace DIPatternSample
{
/// <summary>
/// 何かしらのログを出力するためのサービス
/// </summary>
public class OutputService
{
private readonly ILogger _logger; // ロガーのインターフェースを持つ.
/// <summary>
/// コンストラクタ
/// </summary>
/// <param name="logger">ログの書き込みを行うためのサービスを受け取る</param>
public OutputService(ILogger logger)
{
// 渡ってきた logger が null である可能性もあるので例外を仕込んでおこう.
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
}
/// <summary>
/// ログの書き込みを行います
/// </summary>
public void Write()
{
// 利用者側は _logger の実体を知る必要はありません.
_logger.Write("ログを出力するよ!");
}
}
}
こちらの例では OutputService
が ConsoleLogger
に直接依存することはなくなっていますね。しかし、引数に渡すインスタンスの生成は利用者が行っています。
もしコンストラクタで受け取る引数が増えていった場合など OutputService
のインスタンスを生成する度に引数のインスタンスを生成して渡してあげることになり大変そうですね。
DIコンテナを使用する場合
最後にDIコンテナを使用する場合のコードです。
using System.ComponentModel.Composition.Hosting;
namespace DIContainerSample
{
public class Program
{
static void Main(string[] args)
{
// コンテナに含めたいクラスの型をカタログに定義します.
// ここに定義した型は必要な時に[PartCreationPolicy]属性にしたがってコンテナがインスタンスを自動で生成してくれます.
var catalog = new TypeCatalog(
typeof(ConsoleLogger),
typeof(OutputService));
// カタログ情報をコンテナに渡して生成します.
var container = new CompositionContainer(catalog);
// ここでコンテナから IOutputService を取得しようとします.
// コンテナは IOutputService のインスタンスを生成するために必要な依存関係を洗い出し、
// 自動でインスタンスを生成しようとします.
// 大体の流れとしてはこうです.
//
// ・上記のカタログで IOutputService を継承してエクスポートしている OutputService が定義されいます.
// ・コンテナはこのクラスの[ImportingConstructor]属性が指定されているコンストラクタを探します.
// → コンストラクタが一つも存在しな場合はデフォルトコンストラクタが使用されます.
// ・OutputService の[ImportingConstructor]属性の付いたコンストラクタには ILogger を受け取るように指示されています.
// ・コンテナはカタログから ILogger を継承してエクスポートされているクラスの型を探します.
// ・コンテナは ConsoleLogger のインスタンスを作成し、そのインスタンス使用して OutputService を生成します.
//
// [PartCreationPolicy]属性はパーツを作成する際のルールのようなものです.
// 今回は ConsoleLogger, OutputService 共に CreationPolicy.Shared が指定されているため、GetExportedValue
// で受け取る際に共通のインスタンスを返してくれます. (Singletonような扱いですね)
var outputService = container.GetExportedValue<IOutputService>();
outputService.Write();
}
}
}
using System;
using System.ComponentModel.Composition;
namespace DIContainerSample
{
/// <summary>
/// ログをコンソール上に表示するためのサービス
/// </summary>
[Export(typeof(ILogger))]
[PartCreationPolicy(CreationPolicy.Shared)]
public class ConsoleLogger : ILogger
{
/// <summary>
/// メッセージをロガーに送ります
/// </summary>
/// <param name="message">メッセージ文</param>
public void Write(string message)
{
Console.WriteLine(message);
}
}
}
using System.ComponentModel.Composition;
namespace DIContainerSample
{
/// <summary>
/// 何かしらのログを出力するためのサービス
/// </summary>
[Export(typeof(IOutputService))]
[PartCreationPolicy(CreationPolicy.Shared)]
public class OutputService : IOutputService
{
private readonly ILogger _logger; // ロガーのインターフェースを持つ.
/// <summary>
/// コンストラクタ
/// </summary>
/// <param name="logger">ログの書き込みを行うためのサービスを受け取る</param>
[ImportingConstructor]
public OutputService(ILogger logger)
{
// [ImportingConstructor]属性がついているコンストラクタの引数では、
// [Export]属性でエクスポートされているサービスのインスタンスを受け取ることができます.
// 引数に指定している型がコンテナ上に存在しない場合はエラーとなります.
// また循環しないように注意しましょう.
// 例えば、受け取るロガーのコンストラクタでIOutputServiceを受け取っている場合、循環となりエラーになります.
//
// この時点で logger が null でないことは保証されています.
_logger = logger;
}
/// <summary>
/// ログの書き込みを行います
/// </summary>
public void Write()
{
// 利用者側は _logger の実体を知る必要はありません.
_logger.Write("ログを出力するよ!");
}
}
}
こちらの例では利用者はインスタンスの生成は行っていません。インスタンスの生成を行うのはDIコンテナです。
今回の例では、DIパターンを使用していない方がシンプルなように見えますが、例えば
OutputService
のようにロガーを持つクラスが複数ある場合ConsoleLogger
ではなくFileLogger
を使用するように変えたい場合- こういった依存性のあるクラスが増えていった場合
などを考えてみてください。
クラス間の依存関係が増えるごとに利用者がコードを書き換える箇所も増えていきますし、複数人で作業している場合はもっと大変ですね。
DIパターンを使用することでクラスから依存性を取り除くことができ、DIコンテナを使用することで利用者は依存関係の解決をDIコンテナに任せ、欲しいインスタンスを外から受け取るだけでよくなります。
もし、ILogger の実装を別のものに変えたい場合はカタログに指定する型を変更すればいいですし、コンストラクタで受け取りたいインスタンスが増えたとしても、カタログにエクスポート指定された型が定義されていればDIコンテナが解決してくれます。
最後に
本記事では依存性をクラスから取り除くための方法として、DIパターンがあるということ。DIパターンを実現するためにDIコンテナというフレームワークがあることをご紹介しました。
今回ご紹介したのはDIコンテナのごく一部です。もし興味を持っていただけたならぜひご自身で実装してみてください!
簡単な紹介となりましたが、本記事が少しでもどなたかの参考になれば幸いです。
以上、最後まで読んでいただきありがとうございました!