【DI】依存性の注入について（基礎理解）

プログラミングを始めて大体約1年半になる者です。
現在は複数社でインターンとしてサーバーサイドのエンジニアをしています。

今回はDI(Dependency Injection)のメリットと実装方法を簡単にまとめます。

最初に触った言語がPHP(Laravel)で、それを習得する際によくDIが出てきていて理解に苦しんだことを思い出し、概要を掴める(中上級を理解する土台)程度にまとめます。

クリーンアーキテクチャのSOLID原則などと言った文脈でよく聞きますが、自分も理解が曖昧なので深いことは割愛させていただきます。(涙)

## DIとは

その名の通り**簡単に言えば依存先を外から入れてもらおう**、ということかなと思います。

このメリットとしては以下のような点があります。
- 外から注入することで、依存先の切り替えが楽になる。
- モックのクラスを作れるので、クラス単体でのテストが楽になる。

パッとしないと思うので実装例で見ていきます。

### DIを使わない例

まずはDIを利用していない例です。
あまり現実的ではないコードですが、以下のServiceクラスとRepositoryクラスで考えていきます。
また、今回の説明にはpythonを使っています。pythonの技術的知識がなくても大丈夫です。


#### **実装の概要**
- main.pyでuserの更新データを受け取り、serviceクラスのupdateに関するメソッドを呼び出す。

- serviceのupdateメソッドでrepository層のupdateに関するメソッドを呼び出してDB操作をする。

#### **実装**

(今回使用するDBはfirebaseの想定です。)
**main.py**
```python
handle_user_data = Service()
handle_user_data.update_user_data(data)
```

**Serviceクラス**
```python
import FirebaseRepository

class Service():
	def __init__(self):
		self.repository = Repository()

	def update_user_data(self, data):
		# 何かしらの処理
		self.repository.update(data)
```

**FirebaseRepositoryクラス**
```python
class FirebaseRepository():
	def __init__(self):
		self.db = FirebaseManager.get_instance().db.collection('db')

	def update(self, data):
		self.db.document(data['user_id']).update(data)
```
memo: self.dbはFirebaseでのやり取りを司っているくらいに理解していただければ


#### **コードの概要**

- mainファイルではServiceクラスを呼び出している。
- ServiceクラスはRepositoryクラスをimportしている ⇒ **Repositoryクラスに依存している**

#### **このコードの悪い点**

##### Serviceクラス単体でのテストができない
Repositoryクラスで不具合があった場合、Serviceクラスが機能していても失敗します。


##### DB種類の入れ替えがめんどくさい
ここまで規模が小さいとなんてことはありませんが、実際のプロジェクトで大量のメソッドがある場合少し面倒かもしれません。
変更箇所が多くなったり、クラス名を変えたりメソッド名がfirebase用のものになっていた暁には全ての依存先に変更の影響が出ます。
こちらは最初のうちは少し想像が難しいかもしれないです。

DIを使えば依存先クラスを変更するだけで済むので、変更が最小限に抑えられます。後ほど記載します。

<br />

### DIを使う例

実装内容は同じでDIを使った実装をしていきます。
DIでは主に、**抽象に依存させる**・**オブジェクトを注入する**と言ったことがメインになります。

おそらく具体的実装例を見た方が理解が早いかと思うので早速書いていきます。
また実装で作る内容自体は先ほどと同じです。

memo: 抽象クラスではなくInterfaceを使いますが、今回はイメージ重視なのでそこらへんの細かい定義や違いは飛ばしています。

#### **実装**
**main.py**

```python
firebase_repository = FirebaseRepository()
handle_user_data = Service(firebase_repository)
handle_user_data.update_user_data(data)
```

**Serviceクラス**

```python
import IRepository

class Service():
	def __init__(self, repository: IRepository):
		self.repository = repository

	def update_user_data(self, data):
		# 何かしらの処理
		self.repository.update(data)
```

**IRepositoryクラス**(追加)(Interface)

```python
from abc import ABC, abstractclassmethod

class IRepository(ABC):

	@abstractclassmethod
	def update(self, data):
		raise NotImplementedError()
```

memo: abstractとありますが、機能はInterfaceと同等です。ABCはこのクラスを基底クラス扱いにするためのものです。言語によってはInterfaceクラスが使用できると思うので、それを使います。

**FirebaseRepositoryクラス**

```python
import IRepository

class FirebaseRepository(IRepository):
	def __init__(self):
		self.db = FirebaseManager.get_instance().db.collection('db')

	def update(self, data):
		self.db.document(data['user_id']).update(data)
```

#### **コードの概要**

- main.pyでFirebaseRepository(使いたいrepositoryオブジェクト)を注入したServiceクラスをインスタンス化
- ServiceクラスはFirebaseRepositoryには依存せず、抽象に依存している⇒FirebaseRepositoryでなくてもIRepositoryクラスを継承したものなら注入することができる
- 今回の場合はServiceクラスのupdateメソッドで、FirebaseRepositoryのupdateメソッドを呼び出してDB操作をしている

#### **DIがない時との違い**

- FirebaseRepositoryはIRepositoryクラス(抽象)を継承している
- ServiceクラスはFirebaseRepositoryに直接依存するのではなく、抽象(FirebaseRepositoryの継承元であるIRepositoryクラス)に依存している
- Serviceクラスをインスタンス化する際にはFirebaseRepositoryを注入している

#### **良い点**

- Serviceクラスを抽象に依存させたことにより、Serviceクラス単体でのテストの時に、テスト用のモックRepositoryクラスを作って実行できる
⇒ ServiceクラスはIRepositoryクラスを継承しているものなら注入できる
⇒ モック用クラスを作って、テスト時にServiceクラスに注入できる
⇒ Repositoryクラスにもし異常があってもServiceクラスの単体テストで落ちることはなくなる

<br />

- 使用するRepositoryクラス(この場合はFirebaseRepository)を抽象クラスを継承させることにより、他のDBに変えたい時に、同じクラスを継承したものを作ってそれを注入すれば良いだけになる。
⇒ 変更の柔軟性が向上

<br />

## 終わりに

依存先を切り替えやすくすることの意義をしっかり理解すると、DIの恩恵がスッと入って理解しやすいだろうなと思いました。
小規模アプリケーションの場合であったり、大規模変更が少ない場合は想像しにくいかと思います。（僕は最初訳分かりませんでした。）
また、多くのフレームワークで実装されているDIコンテナというものを使えば、今回は外からインスタンス化して実装クラスを注入しましたが、Interface呼び出し時に自動的に実装クラスを呼び出すようにする等のこともできます。
ここら辺ちゃんと理解するとクリーンアーキテクチャとかの理解が進むかなと思います。(自分もまだまだです….)
もっと良い説明方法やもし誤った解釈・表現があれば指摘お願いいたします。

## 参考

https://zenn.dev/plhr7/articles/36ddd240ccbb97