「何故DIコンテナが必要なのか」について、自分なりに説明を試みる。
目次
- DIコンテナ以前に、そもそも「DI」がなぜ必要なのか
- 問題点:「DIでは一体誰が
new
するべきか?」 -
new
するためのクラスを別に作る -
new
をDIコンテナに任せる - Q&A
1. DIコンテナ以前に、そもそも「DI」がなぜ必要なのか
そもそも「DI(依存性注入)」がなぜ必要なのかを説明する。
下記のように、ハードウェア制御クラスGreatHardware
があり、クラスLowLayer
はそれを利用しているとする。クラスGreatHardware
は、実際のハードウェアがないと動かないとする。
この構成には問題がある。クラスLowLayer
が、実際のハードウェアがないと動かないクラスGreatHardware
を直接new
してしまっているため、実際のハードウェアがないとテストできない。
// 構成例1
class GreatHardware {
somethingGreat(){
// 何かハードウェア依存の処理を行う
}
}
class LowLayer {
hardware:GreatHardware;
constructor(){
this.hardware = new GreatHardware();
}
doHoge() {
this.hardware.somethingGreat();
}
}
これを解決するには、下記の構成例2のように「DI(依存性注入)」を使うことができる。ここでは、LowLayer
のコンストラクタからGreatHardwareのインスタンスを注入している。これによりインターフェースIGreatHardware
を実装したモックをLowLayer
のコンストラクタに注入すれば実際のハードウェアがなくてもLowLayer
のテストができるということになる。
// 構成例2
interface IGreatHardware {
somethingGreat();
}
class GreatHardwareImpl implements IGreatHardware {
somethingGreat(){
// 何かハードウェア依存の処理を行う
}
}
class LowLayer {
hardware:IGreatHardware;
constructor(hardware:IGreatHardware){
this.hardware = hardware;
}
doHoge() {
this.hardware.somethingGreat();
}
}
2. 問題点:「DIでは一体誰がnew
するべきか?」
しかし、めでたしめでたしとはいかない。構成例2を実際に動かすには、誰かがGreatHardware
をnew
してLowLayer
に渡さなくてはならない。
例えば構成例3が考えられるだろう。しかし、これでは構成例1と同じ問題が起こる。結局MiddleLayer
がGreatHardware
をnew
しなければならず、MiddleLayer
がGreatHardware
に依存してしまうからだ。
// 構成例3
interface IGreatHardware {
somethingGreat();
}
class GreatHardwareImpl implements IGreatHardware {
somethingGreat(){
// 何かハードウェア依存の処理を行う
}
}
class LowLayer {
hardware:IGreatHardware;
constructor(hardware:IGreatHardware){
this.hardware = hardware;
}
doHoge() {
this.hardware.somethingGreat();
}
}
class MiddleLayer {
lowLayer:LowLayer;
constructor(){
this.lowLayer = new LowLayer(new GreatHardware());// <- 結局newしている
}
doFuga(){
this.lowLayer.doHoge();
}
}
ではさらに上のレイヤーにGreatHardware
をnew
する責任を持たせるようにしたらどうだろうか(構成例4)。
しかし、結局これだとUIレイヤーが具体的なハードウェアがないと動かないクラスに依存することになってしまった。
構成例1よりもまずい状況なのではないだろうか?
// 構成例4
interface IGreatHardware {
somethingGreat();
}
class GreatHardwareImpl implements IGreatHardware {
somethingGreat(){
// 何かハードウェア依存の処理を行う
}
}
interface ILowLayer {
doHoge();
}
class LowLayerImpl implements ILowLayer {
hardware:IGreatHardware;
constructor(hardware:IGreatHardware){
this.hardware = hardware;
}
doHoge() {
this.hardware.somethingGreat();
}
}
interface IMiddleLayer {
doFuga();
}
class MiddleLayerImpl implements IMiddleLayer {
lowLayer:ILowLayer;
constructor(lowLayer:ILowLayer){
this.lowLayer = lowLayer;
}
doFuga(){
this.lowLayer.doHoge();
}
}
interface IHighLayer {
doFoo();
}
class HighLayerImpl implements IHighLayer {
middleLayer:IMiddleLayer;
constructor(middleLayer:IMiddleLayer){
this.middleLayer = middleLayer;
}
doFoo(){
this.middleLayer.doHoge();
}
}
class UIController {
highLayer:IHighLayer;
constructor(){
this.highLayer = new HighLayerImpl(new MiddleLayerImpl(new LowLayerImpl(new GreatHardwareImpl())));// <- UIが実際のハードウェアがないと動かないクラスに依存している。
this.highLayer.doFoo();
}
}
3. new
するためのクラスを別に作る
構成例4を見たとき、
new HighLayerImpl(new MiddleLayerImpl(new LowLayerImpl(new GreatHardwareImpl())));
この部分をさらに誰か別のクラスの責任にしたらいいのでは?と思う人がいると思う。
それをやって見たのが構成例5である(UIControllerより下のレイヤはこれまでの構成例と同じなので省略)。
// 構成例5
class UIController {
highLayer:IHighLayer;
constructor(){
this.highLayer = GreatFactory.createHithLayer();
this.highLayer.doFoo();
}
}
class GreatFactory {
static createHighLayer(){
return new HighLayerImpl(new MiddleLayerImpl(new LowLayerImpl(new GreatHardwareImpl())));
}
}
さらに一貫性の観点から、UIController
インスタンスの生成もGreatFactory
が任されると考えてみよう。構成例6である。
// 構成例6
class UIController {
highLayer:IHighLayer;
constructor(highLayer:IHighLayer){
this.highLayer = highLayer;
this.highLayer.doFoo();
}
}
class GreatFactory {
static createUiController(){
return new UIController(new HighLayerImpl(new MiddleLayerImpl(new LowLayerImpl(new GreatHardwareImpl()))));
}
}
これによって全てのレイヤーが分離し、相互依存がない状態になった。GreatFactory以外のクラスの依存性は外部から注入でき、モックテストができる。
ただ、最後の問題はGreatFactory
だ。ここのメンテナンス性には問題がある。例えば、MiddleLayerImpl
のコンストラクタ引数が増加したらどうだろうか。メンテナンスが大変ではないだろうか。
4. new
をDIコンテナに任せる
これをDIコンテナを使って書き直すとすごく簡単になる。
ここでは簡単のため、架空のDIコンテナを用いる。
この架空のDIコンテナは、コンストラクタの前に@Inject()
を記載すると、別途書いた設定通りに引数の型に対応するインスタンスを生成してセットしてくれる。TypeScriptのinterface
は実行時には認識できないが、何かの魔法で認識されるものとする。@Inject
はDIコンテナが動いていない場合は(例えばテストの時など)、何も書いていないのと同じになる。
このDIコンテナの設定ファイルは下記のように書く。=
の左辺の型には、右辺のクラスのインスタンスが渡される。
IHighLayer=HighLayerImpl
IMiddleLayer=MiddleLayerImpl
ILowLayer=LowLayerImpl
IGreatHardware=GreatHardwareImpl
構成例7にこのDIコンテナを使った例を示す。
// 構成例7
interface IGreatHardware {
somethingGreat();
}
class GreatHardwareImpl implements IGreatHardware {
somethingGreat(){
// 何かハードウェア依存の処理を行う
}
}
interface ILowLayer {
doHoge();
}
class LowLayerImpl implements ILowLayer {
hardware:IGreatHardware;
@Inject()
constructor(hardware:IGreatHardware){
this.hardware = hardware;
}
doHoge() {
this.hardware.somethingGreat();
}
}
interface IMiddleLayer {
doFuga();
}
class MiddleLayerImpl implements IMiddleLayer {
lowLayer:ILowLayer;
@Inject()
constructor(lowLayer:ILowLayer){
this.lowLayer = lowLayer;
}
doFuga(){
this.lowLayer.doHoge();
}
}
interface IHighLayer {
doFoo();
}
class HighLayerImpl implements IHighLayer {
middleLayer:IMiddleLayer;
@Inject()
constructor(middleLayer:IMiddleLayer){
this.middleLayer = middleLayer;
}
doFoo(){
this.middleLayer.doHoge();
}
}
class UIController {
highLayer:IHighLayer;
@Inject()
constructor(highLayer:IHighLayer){
this.highLayer = highLayer;
this.highLayer.doFoo();
}
}
これにより設定通りにインスタンスを生成してくれる。
メンテナンス性も向上している。下記のようにMiddleLayerImplのコンストラクタの引数が増えたときも、
class MiddleLayerImpl implements IMiddleLayer {
lowLayer:ILowLayer;
@Inject()
constructor(lowLayer:ILowLayer, otherLayer:IOtherLayer){ // <- otherLayerが増えた
this.lowLayer = lowLayer;
}
doFuga(){
this.lowLayer.doHoge();
}
}
下記のようにDIコンテナの設定に追記すれば正しくインスタンスを生成してセットしてくれる。
IHighLayer=HighLayerImpl
IMiddleLayer=MiddleLayerImpl
ILowLayer=LowLayerImpl
IGreatHardware=GreatHardwareImpl
IOtherLayer=OtherLayerImpl // <- otherLayerの設定を追加
5. Q&A
- Q1. 大げさすぎるように思える。なぜ本番のシステムにDIコンテナという複雑な仕組みを持ち込む必要があるのだろうか。むしろ、テストの時だけ使う仕組みを用意して、
GreatHardware
を差し替えればそれで済むのではないだろうか。 - A. 「テストの時だけに使う仕組み」はむしろ危険なことが多い。テストの時だけにしか使わないから、もしバグがあったときに非常に見つかりにくい。テストの時はできるだけ仕組みを使わないべきだと思う。その点、DIコンテナを使った構成であれば、テストのときはクラスはただのクラスであり、単純にテストすれば良いので非常にシンプルになる。