概要
この記事は モバイルファクトリー Advent Calendar 2017 11日目の記事です。
10日目の記事は @tsukumaruさんの 「OpenMapTilesとgo-staticmaps」 でした。
本記事ではUnityのDIフレームワークであるZenjectからDIについて学んだことをツラツラと書いていくものです。
DI(Dependency Injection)とは
別の呼び方としてはIOC(Inversion of Control: 制御の反転)とも呼ばれるそうです。
--- 追記 2017/12/12 ---
IOCの実装手法としてDIが存在している、という関係が正しそうです。
https://ja.wikipedia.org/wiki/%E5%88%B6%E5%BE%A1%E3%81%AE%E5%8F%8D%E8%BB%A2
--- 追記終わり ---
Dependency Injectionは一般的に「依存性の注入」と訳されます。
依存性の注入。字面だけ見ても一体何なのかピンときません。
調べてみると、どうやらDependencyは「依存性」と訳すのではなく、「必要なもの」というような訳し方が正しそうです。
つまり、あるクラスAに対して、そのクラスAが必要としているクラスBを注入する、ということのようです。
その前に「依存している」状態とはどういうことなのか。
例として以下のようなコードを示します。
class Service
{
private SomeService someService;
void Service()
{
this.someService = new SomeService();
}
public void Hello()
{
this.someService.Hello();
}
}
class SomeService
{
public void Hello()
{
...
}
}
このようなコードを書くと以下のような理由でServiceとSomeServiceは密に依存していると言えます。
- ServiceはSomeServiceの生成と利用の2つの役割を担っている
- SomeServiceの変更がServiceにも影響し得る
- ServiceはSomeServiceが存在しないと動かない
密に依存していると以下のように困ってしまいます。
- SomeServiceを変更する際の影響範囲がServiceまで及ぶ
- ServiceのテストがしたいのにSomeServiceのことを考える必要がある
このような状態にしないためにDIパターンというデザインパターンが存在します。
先ほどのコードにDIパターンを適用するとすると、以下のようになります。
class Service
{
private SomeService someService;
void Service(SomeService someService)
{
this.someService = someService;
}
public void Hello()
{
this.someService.Hello();
}
}
class SomeService
{
public void Hello()
{
...
}
}
やったことと言えば、SomeServiceをコンストラクタの引数として渡すようにしただけです。
ですが、これがDIパターンと呼ばれるデザインパターンとなります。
このようにすると、以下のように依存関係が疎になります。
-
ServiceはSomeServiceの生成と利用の2つの役割を担っている- SomeServiceの生成を外部に移譲したことで、ServiceはSomeServiceを利用する役割しか持たない
- SomeServiceの変更がServiceにも影響し得る
- ServiceはSomeServiceが存在しないと動かない
以下のように困り事が減ります。
- SomeServiceを変更する際の影響範囲がServiceまで及ぶ
-
ServiceのテストがしたいのにSomeServiceのことを考える必要がある- SomeServiceについて考える必要はあるが、Mockを渡してあげることでテストが比較的容易になる
さらに、SomeServiceにInterfaceを定義し、そのInterface型で受け取るようにしてみます。
class Service
{
private ISomeService someService;
void Service(ISomeService someService)
{
this.someService = someService;
}
public void Hello()
{
this.someService.Hello();
}
}
interface ISomeService
{
void Hello();
}
class SomeService: ISomeService
{
public void Hello()
{
...
}
}
このようにすると、以下のように依存関係が疎になります。
-
SomeServiceの生成を外部に移譲したことで、ServiceはSomeServiceを利用する役割しか持たない- ISomeServiceの生成を外部に移譲したことで、ServiceはISomeServiceを利用する役割しか持たない
-
SomeServiceの変更がServiceにも影響し得る- ISomeServiceの定義が変更されない限り、SomeServiceの変更はServiceに影響しない
-
ServiceはSomeServiceが存在しないと動かない- Service内部で利用するsomeServiceはISomeServiceを実装していれば何でも良い
以下のように困り事が減ります。
-
SomeServiceを変更する際の影響範囲がServiceまで及ぶ- ISomeServiceの定義が変更されない限り、SomeServiceの変更はServiceに影響しない
-
SomeServiceについて考える必要はあるが、Mockを渡してあげることでテストが比較的容易になる- ISomeServiceの定義が変更されない限り、テストに影響がほとんどない
これがDIパターンの効能ということになります。
DIコンテナとは
DIパターンが簡単に書けるなら、別にフレームワークとか必要ないのでは?と思いますが、そうも行かないようです。
例えば、先ほどのServiceを利用する側を考えてみます。
class SuperService
{
private Service service;
void SuperService()
{
this.service = new Service(new SomeService());
}
}
SuperServiceがServiceに依存してしまいました。
DIパターンを適用します。
class SuperService
{
private IService service;
void SuperService(IService service)
{
this.service = service;
}
}
これでよし。
class HyperService
{
private SuperService superService;
void HyperService()
{
this.superService = new SuperService(new Service(new SomeService()));
}
}
このまま行くとプログラムのエントリーポイントまで遡りそうな勢いです。
これは極端な例ですが、このようにプロジェクトの規模が大きくなってくると依存関係が複雑になり、管理が難しくなってきます。
そこで登場するのがDIコンテナを含む、DIフレームワークになります。
DIコンテナを利用することによって、依存関係のみの記述によって各オブジェクトに対して依存先のオブジェクトを良しなに渡してくれます。
Zenjectを例とすると以下のような感じです。
public class ServiceInstaller : MonoInstaller<ServiceInstaller>
{
public override void InstallBindings()
{
Container.Bind<ISomeService>().To<SomeService>().AsSingle();
}
}
class Service
{
[Inject]
private ISomeService someService;
public void Hello()
{
this.someService.Hello();
}
}
interface ISomeService
{
void Hello();
}
class SomeService: ISomeService
{
public void Hello()
{
...
}
}
きちんと動かすにはもう少し実装が必要ですが、大枠はこんな感じです。
初めにInstallerで依存関係を定義。
この場合はISomeServiceを実装しているSomeServiceを各Injectしているpropertyに対して注入します。
先ほどのコンストラクタからISomeServiceを受け取る処理を書かずに済んでいます。
DIフレームワーク導入の利点
依存関係が疎になりやすく、具体的に以下のような利点が挙げられます。
- リファクタリングしやすくなる
- モジュラープログラミングしやすくなる
- テストしやすくなる
つまりは、オブジェクト指向プログラミングにおける5つの原則(SOLID原則)を守りやすくなるということですね。
まとめ
ZenjectのREADMEに書いてありました。
Zenject要素ほとんどありませんでしたが、ZenjectのREADMEが思いの外読み物としても面白かったので、勉強がてらに紹介した次第です。
次の担当は @yashims85 さんです。よろしくお願いします。