プログラミングを始めて大体約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
handle_user_data = Service()
handle_user_data.update_user_data(data)
Serviceクラス
import FirebaseRepository
class Service():
def __init__(self):
self.repository = Repository()
def update_user_data(self, data):
# 何かしらの処理
self.repository.update(data)
FirebaseRepositoryクラス
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を使えば依存先クラスを変更するだけで済むので、変更が最小限に抑えられます。後ほど記載します。
DIを使う例
実装内容は同じでDIを使った実装をしていきます。
DIでは主に、抽象に依存させる・オブジェクトを注入すると言ったことがメインになります。
おそらく具体的実装例を見た方が理解が早いかと思うので早速書いていきます。
また実装で作る内容自体は先ほどと同じです。
memo: 抽象クラスではなくInterfaceを使いますが、今回はイメージ重視なのでそこらへんの細かい定義や違いは飛ばしています。
実装
main.py
firebase_repository = FirebaseRepository()
handle_user_data = Service(firebase_repository)
handle_user_data.update_user_data(data)
Serviceクラス
import IRepository
class Service():
def __init__(self, repository: IRepository):
self.repository = repository
def update_user_data(self, data):
# 何かしらの処理
self.repository.update(data)
IRepositoryクラス(追加)(Interface)
from abc import ABC, abstractclassmethod
class IRepository(ABC):
@abstractclassmethod
def update(self, data):
raise NotImplementedError()
memo: abstractとありますが、機能はInterfaceと同等です。ABCはこのクラスを基底クラス扱いにするためのものです。言語によってはInterfaceクラスが使用できると思うので、それを使います。
FirebaseRepositoryクラス
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クラスの単体テストで落ちることはなくなる
- 使用するRepositoryクラス(この場合はFirebaseRepository)を抽象クラスを継承させることにより、他のDBに変えたい時に、同じクラスを継承したものを作ってそれを注入すれば良いだけになる。
⇒ 変更の柔軟性が向上
終わりに
依存先を切り替えやすくすることの意義をしっかり理解すると、DIの恩恵がスッと入って理解しやすいだろうなと思いました。
小規模アプリケーションの場合であったり、大規模変更が少ない場合は想像しにくいかと思います。(僕は最初訳分かりませんでした。)
また、多くのフレームワークで実装されているDIコンテナというものを使えば、今回は外からインスタンス化して実装クラスを注入しましたが、Interface呼び出し時に自動的に実装クラスを呼び出すようにする等のこともできます。
ここら辺ちゃんと理解するとクリーンアーキテクチャとかの理解が進むかなと思います。(自分もまだまだです….)
もっと良い説明方法やもし誤った解釈・表現があれば指摘お願いいたします。
参考