プログラミング初心者がアーキテクトっぽく語る

見苦しい記事も多数あるとは思いますが訂正しつつブログと共に成長していければと思います

Dependency Injectionパターン

設計においてクラス間の独立性を高めることは重要である。

クラス間の独立性を高める方式の1つにDependency Injectionパターンがある。今回はDependency Injectionパターンについて説明する。


概要

日本語には「依存性注入」と訳される。この言葉からDIがどういうものか理解するのは難しい。むしろ「依存性」を「注入」するとか怪しい香りしかしない。

が、「依存性」を「オブジェクト」と捉えると理解しやすい。つまりはオブジェクトを外部から注入すること全般を指す言葉だ。

例えばオブジェクトAがオブジェクトBを利用していたとする。このとき、オブジェクトAにオブジェクトBを生成(new)させるのではなく、別のところでBを生成してAに渡してあげるのがDIだ。

BをAに注入する方式は複数ある。

Bと同じInterfaceを実装したオブジェクトCをAに注入しても問題なく動作するため、AはBに依存しなくなったと言える。

DIを使わなかった場合、(A)→(B)だった依存関係が、DIにより(A→Inteface)←(B)という依存関係になり、Bから見ると依存の方向性が逆転する。

もともとInversion of Controlの実現方式の1つとして登場したが、IoCそのものと区別するためにDependency Injectionパターンと命名された。


登場人物

1. Service

  • 注入したいオブジェクト
  • オブジェクトB、Cに相当

2. Client

  • Serviceを利用したいオブジェクト
  • オブジェクトAに相当

3. Injector

  • ServiceをClientに注入する役

それぞれの関係性

  • ClientとServiceはInjectorについては知らない
  • ClientはServiceの作成方法を知らない
  • ServiceはClientのことを意識しない
  • Injectorは全て知っている

メリット

ServiceとClientの依存関係を解消し、独立性を高めたことがメリットだ。

独立性が高いということは開発者目線で見れば交換容易性が高いということだ。変更に強いだけなく、ServiceをMockに置き換えて容易にClientを単体テストすることができるようになる。

設計者目線で見れば適切な問題領域の分割(Seperation of Concern)がしやすいということであり、それはSOLID原則のSRPを意識した設計ができるということだ。また依存関係が逆転することを利用してSOLID原則のDIPDependency Inversion Principle)を意識した依存関係の最適化もできる。

Serviceが限られたリソースだった場合にはInjectorで統合的にリソース管理させることができる。

Client終了時にServiceの解放処理を確実に実行したい場合はやはりInjectorに任せることができる。


デメリット

デメリットとしては部品が多くなることだ(これはデザインパターン全般に言えることで、DIパターン固有のデメリットではない)。

全ての依存関係に盲目的に適用するのではなく、依存関係の弱さや方向性が重要になる境界や、単体テストが難しいところから採用を検討するのがよい。

なおDIコンテナと呼ばれるフレームワークを利用するとInjector機能の大部分をフレームワークが担ってくれるので、気軽にDIパターンを利用できるようになる。


注入の方式

1. Constructor Injection

  • InjectorがClientのConstructorの引数としてServiceを渡す。
  • 現在ではBest Practiceと見なされている様子。

2. Setter Injection

  • Client生成後にInjectorがClientのsetterメソッドでServiceを渡す。
  • Springで多用されていたがClientがMutableになることから最近ではBad Practiceと認識されている様子。

3. Field Injection

  • Client生成後にInjectorがClientのプロパティへ直接アクセスしてServiceを渡す。
  • フレームワークとして実装しやすいことから簡易なDIコンテナでよく見るパターン。
  • 情報隠蔽の観点からプロパティをpublicにすることは避けたい。
  • リフレクションを使わなくてはならない点がデメリット。

4. Interface Injection

  • 「インジェクション用のインタフェースを定義して利用する方法」とのこと。
  • サイトによってConstructor Injectionのようだったり、Service Locatorパターンのようだったりして、よく理解できていない。

Constructor Injectionの例

Constructor Injectionの例を以下に示す。

Setter InjectionとField Injectionは自明だと思うのでここでは省略する。

Interface Injectionについては理解できたら検討する。

1. DIを使わない例

Clientの中でServiceオブジェクトを生成している(9行目)。

Clientが使用するServiceオブジェクトを他のオブジェクトに交換するにはClientクラスの書き換えが必要になる。

# Service
class Service():
  def execute(self):
    <省略>

# Client
class Client():
  def doSomething(self):
    service = Service()     # これをClientクラスの外に出したい
    service.execute()

# Injector
if __name__ == '__main__':
  client = Client()
  client.doSomething()

2. Constructor Injectionを利用した例

サービスオブジェクトの作成がClientクラスからInjectorクラスに移動した(16行目)。

Clientが使用するServiceオブジェクトを他のオブジェクトに交換するにはInjectorクラスを書き換えればよい。

# Service
class Service():
  def execute(self):
    <省略>

# Client
class Client():
  def __init__(self, service):
    this.service = service      

  def doSomething(self):
    this.service.execute()

# Injector
if __name__ == '__main__':
  service = Service()        # ここに移動した
  client = Client(service)   # ClientのConstructorに渡している
  client.doSomething()

3. Constructor Injectionを利用した例(Interfaceあり)

最後にInterfaceを使ったConstructor Injectionパターンを紹介する。

以下の例ではIServiceを実装している他のクラスに容易に置き換えが可能となっている。

動的型付け言語ならInterfaceなしでDuck Typingすれば同じことができるが、Javaのような静的型付け言語の場合はInterfaceを利用したこの例が参考になるだろう。

# Interface
class IService:
  @abstractmethod
  def execute(self):
    pass

# Service
class ServiceA(IService):
  def execute(self):
    <省略>

# Client
class Client():
  def __init__(self, service: IService):
    this.service = service      

  def doSomething(self):
    this.service.execute()

# Injector
if __name__ == '__main__':
  service = ServiceA()
  client = Client(service)
  client.doSomething()