はじめに
どうすればソフトウェアを変更容易にできるかというのは重要な課題である。ISO/IEC 9126(もしくは、改定されたISO/IEC 25010)というソフトウェア品質の評価に関する国際規格の中にも保守性 (maintainability) という変更に対するコストに関わる特性がある。
変更容易にするための1つの手法として、DI(Dependency Injection)というものがある。これはモジュール同士の結合を疎結合にして、変更の影響範囲を狭める手法である。この記事ではDIやサービスロケータなどについて整理しつつ理解を深めていきたい。
DIをしないHello World!
解決したい課題を具体的な例を用いて説明する。以下はGreeterクラスのコンストラクタでGreetingWordsCreatorをインスタンス化してGreeting()で呼び出すという例だ。
class Program { static void Main(string[] args) { var greeter = new Greeter(); greeter.Greeting(); } } public class Greeter { private GreetingWordsCreator _creator; public Greeter() { this._creator = new GreetingWordsCreator(); } public void Greeting() { Console.WriteLine(this._creator.Create()); } } public class GreetingWordsCreator { public string Create() { return "Hello World!"; } }
Greeterクラスは挨拶をするクラスで、Greeting()を呼び出すと挨拶ができる。そして挨拶の文章を生成するためにGreetingWordsCreatorクラスを使う。
ここで、文章を生成するGreetingWordsCreatorを変更したいとする。例えばnew GreetingWordsCreator()
をnew GreetingWordsCreator("ja-jp")
とすると日本語の文章が帰ってくるだとか、ランダムな文章を生成するRandomWordsCreatorに変更するだとかである。
例えば日本語の文章を帰ってくるように変更をするならば以下のようになる。
public class Greeter { private GreetingWordsCreator _creator; public Greeter() { // ここに変更が生じる this._creator = new GreetingWordsCreator("ja-jp"); } public void Greeting() { Console.WriteLine(this._creator.Create()); } }
このように文章を生成するクラス変更しようとすると、挨拶をするクラスの変更も必要になる*1。
この問題が起きるのはGreeterとGreetingWordsCreatorが密結合になっているからである。このGreeterクラスは文章を生成するクラスがCreate()
で文章を生成できることだけを知っていればいいので、インスタンス化の方法は知らなくてもいい。
解決のアプローチ
以上のような「依存するクラスの変更の影響が依存元にも及ぶ」という問題を解決したい。この問題の解決のアプローチは以下の3つが一般的だろう。
- Factory パターン
- Service Locator パターン
- Dependency Injection パターン
*基本的にインターフェースを伴うので、疎結合に対するインターフェースの効果も寄与する。
Dependency Injection パターン
Dependency Injection パターンは以下のようにGreeterが依存するGreetingWordsCreatorを生成時に渡してやるというものだ。
class Program { static void Main(string[] args) { var greeter = new Greeter(new GreetingWordsCreator()); greeter.Greeting(); } } public class Greeter { private IGreetingWordsCreator _creator; public Greeter(IGreetingWordsCreator creator) { this._creator = creator; } public void Greeting() { Console.WriteLine(this._creator.Create()); } } public class GreetingWordsCreator: IGreetingWordsCreator { public string Create() { return "Hello World!"; } } public interface IGreetingWordsCreator { string Create(); }
これによりnew GreetingWordsCreator("ja-jp")
やRandomWordsCreator
に変更しようがGreeterの変更の必要がなくなる。GreeterはGreetingWordsCreatorのCreate()
を呼び出すだけである。
これが実現できたのは、DIによってGreetingWordsCreatorの生成方法をGreeterが知らなくて良いようになったからである。GreeterはCreate()というインターフェースにのみ依存するようになった*2。
このアプローチが他より優れているのは、テストが容易であり、コンストラクタなどを見れば依存関係がわかりやすい点である。
生成が重い処理の場合はLazyを使ったり、コンストラクタではなくメソッドによる依存注入をするなどが必要になると思われる。
Factory パターン
FactoryパターンはGoFデザインパターンの1つで以下のようにFactoryクラスを介して依存先を取得することで結合度を下げることができる。
class Program { static void Main(string[] args) { var greeter = new Greeter(); greeter.Greeting(); } } public class Greeter { public static IGreetingWordsCreatorFactory Factory { get; set; } = new GreetingWordsCreatorFactory(); private IGreetingWordsCreator _creator; public Greeter() { this._creator = Factory.Create(); } public void Greeting() { Console.WriteLine(this._creator.Create()); } } public class GreetingWordsCreator: IGreetingWordsCreator { public string Create() { return "Hello World!"; } } public class GreetingWordsCreatorFactory : IGreetingWordsCreatorFactory { public IGreetingWordsCreator Create() { return new GreetingWordsCreator(); } } public interface IGreetingWordsCreatorFactory { IGreetingWordsCreator Create(); } public interface IGreetingWordsCreator { string Create(); }
this._creator = Factory.Create();
というようにコンストラクタで依存するIGreetingWordsCreator
を生成することで依存関係を解決している。このFactoryを変更することでGreeterを変更することなく挙動を変えることができるのがわかるだろうか。
DIでは先に依存先の生成をしてコンストラクタで渡すということをしていたが、Factoryではその必要がない。ただし依存関係が新たに増えるというデメリットがある。
Service Locator パターン
Service LocatorパターンもFactoryパターンに似ている。以下のような連想配列にインターフェースとその実装した型を登録しておいて、使う時に呼び出すというようになっている。
class Program { static void Main(string[] args) { // 設定 var locator = new ServiceLocator(); locator.Register<IGreetingWordsCreator, GreetingWordsCreator>(); ServiceLocator.Locator = locator; var greeter = new SLGreeter(); greeter.Greeting(); } } public class Greeter { private IGreetingWordsCreator _creator; public Greeter() { this._creator = ServiceLocator.Resolve<IGreetingWordsCreator>(); } public void Greeting() { Console.WriteLine(this._creator.Create()); } } public class ServiceLocator { public static ServiceLocator Locator { get; set; } = new ServiceLocator(); private IDictionary<Type, Type> _registry = new Dictionary<Type, Type>(); public void Register<TKey, TValue>() { _registry[typeof(TKey)] = typeof(TValue); } public static TKey Resolve<TKey>() { return (TKey)Activator.CreateInstance(Locator._registry[typeof(TKey)]); } } public class GreetingWordsCreator: IGreetingWordsCreator { public string Create() { return "Hello World!"; } } public class GreetingWordsCreatorFactory : IGreetingWordsCreatorFactory { public IGreetingWordsCreator Create() { return new GreetingWordsCreator(); } } public interface IGreetingWordsCreatorFactory { IGreetingWordsCreator Create(); } public interface IGreetingWordsCreator { string Create(); }
これでGreeterを変更することなく挙動を変えることができる。しかし、Factoryと同様に依存関係が増えてしまう。Factoryと違うのはServiceLocatorクラスがあればどんなクラスでも依存先を取得できる。
ServiceLocatorの実装に関しては、stringキーから解決する方法もあるが、型の方がtypoを減らせて良いだろう。ServiceLocatorと_registryをstaticにしてシンプルにするのと、引数も登録できるようにするなどしてもいいかもしれない。
まとめ
- Dependency Injectionにより疎結合にすることができる。
- FactoryパターンやService Locator パターンでも疎結合にすることは可能だが、その場合は依存関係が新たに増える。