依存
モジュールを分割したら、次は個別にテストをすることを考えます。
以下の保存処理を考えましょう。
ただし、前章の条件を満たさない場合はエラーとする
前章の条件
*2022年以前の基準(男性18歳、女性16歳)
これを単純に書くとこうなります。
from dataclasses import dataclass
@dataclass
class User:
name: str
sex: "man" | "woman"
age: Age
income: Income
class Application:
# 適当なDBクライアントを想定
__dbClient: DBClient
def __init__(self) -> void:
__dbClient = DBClient(...)
def createUser(self, user: User) -> void:
if isAcceptable(user):
self.__dbClient.exec('INSERT INTO user VALUES(?, ?, ?, ?)',
(user.name, user.sex, user.age.value, user.income.value))
else:
raise Error("User is not acceptable for this service.")
# 前章と同一ロジック
def isAcceptable(user: User) -> bool:
if user.sex == "man" and user.age.value >= 18 or user.sex == "woman" and user.age.value >= 16:
if user.income.value >= 5:
return true
return false
このApplicationクラスはDBへの保存と、ユーザのチェックという2種類の役割を持っているので、分割してみましょう。
# アプリケーションメインロジック
class Application:
__repository: UserRepository
def __self__(self):
self.__repository = UserRepository()
def createUser(self, user: User) -> void:
if isAcceptable(user):
self.__repository.save(user)
else:
raise Error("User is not acceptable for this service.")
def isAcceptable(user: User) -> bool:
if user.sex == "man" and user.age.value >= 18 or user.sex == "woman" and user.age.value >= 16:
if user.income.value >= 5:
return true
return false
# ユーザデータに責務を持つ
class UserRepository:
# 適当なDBクライアントを想定
__dbClient: DBClient
def __init__(self) -> void:
self.__dbClient = DBClient(...)
def save(self, user: User) -> void:
self.__dbClient.exec('INSERT INTO user VALUES(?, ?, ?, ?)',
(user.name, user.sex, user.age.value, user.income.value))
さて、これは分割できたと言えるでしょうか…?
UserRepositoryクラス
問題ありません。DBが必要ですが、DBがあればこのクラス単独でテストが可能です。
Applicationクラス
内部でUserRepositoryクラスが必ず呼ばれます。言い換えると、ApplicationクラスはUserRepositoryクラスに 依存 しています。
Applicationクラスの動作にはUserRepositoryクラスが必須なので、これは不完全な分割です。
インターフェース
依存をなくすためには、インターフェースを導入します。
オブジェクト指向における 依存性逆転の原則(DIP) を思い出ししてください。
参考: 📄 【オブジェクト指向2024 #9】インターフェース
class Application:
__repository: IUserRepository
# コンストラクタで挿入
def __self__(self, repository: IUserRepository):
self.__repository = repository
def createUser(self, user: User) -> void:
if isAcceptable(user):
self.__repository.save(user)
else:
raise Error("User is not acceptable for this service.")
def isAcceptable(user: User) -> bool:
if user.sex == "man" and user.age.value >= 18 or user.sex == "woman" and user.age.value >= 16:
if user.income.value >= 5:
return true
return false
from abc import ABC, abstractmethod
# インターフェース
class IUserRepository(ABC):
# save()メソッドがあることだけを宣言
@abstractmethod
def save(self, user: User):
raise NotImplementedError()
# インターフェースを実装していることを宣言
class UserRepository(IUserRepository):
# 適当なDBクライアントを想定
__dbClient: DBClient
def __init__(self) -> void:
self.__dbClient = DBClient(...)
def save(self, user: User) -> void:
self.__dbClient.exec('INSERT INTO user VALUES(?, ?, ?, ?)',
(user.name, user.sex, user.age.value, user.income.value))
ポイントは3つ。
-
IUserRepository
インターフェースを用意して、インターフェースを実装する形でUserRepositoryクラスを作る -
Applicationクラスは
IUserRepository
を使うようにする- Applicationクラスは「save()メソッドを持つ何か」を呼び出すことになります
-
IUserRepository
の実体はコンストラクタで外から渡す
こうすることで、Applicationクラスはsave()メソッドしか呼び出せなくなります。言い換えると インターフェースにのみ依存 することになります。
IUserReposiotryインターフェースを実装しているクラスなら何でも良いので、テスト時などでは適当なモックに差し替えて動かすことが可能です。
class MockUserRepository:
# DBの代わりの辞書型
__mockDict: dict[str, User]
def save(self, user: User) -> void:
self.__mockDict[user.name] = user
このように、直接クラスを参照せずにインターフェースを参照することで、実体クラスは 入れ替え可能 になります。
これはテストの差し替えだけでなく、
- DBクライアントライブラリの仕様が変わった
- DBを変えなければいけなくなった
など、実装変更が必要になった場合、一部クラスの差し替えだけで済む(=影響範囲が限定される)ことを意味します。
依存性の注入(Dependency Injection)
インターフェースを導入したことで、実体クラスインスタンスを渡す必要が出ました。
これはクラス呼び出し側で行います。
# IUserRepotitoryインターフェースを満たすクラスを用意して
repository = UserRepository()
# 依存として渡す(=注入)
application = Application(repository)
application.createUser(...)
このように、依存するインスタンス(オブジェクト)を外から与えることを 依存性の注入(Dependency Injection = DI) といい、特にコンストラクタで与えることをコンストラクタインジェクションと呼びます。
DIは依存クラスが増えてくるとだんだん辛くなってくるので、これを自動的にやってくれるライブラリに任せることがあります。
このようなライブラリを DIコンテナ と呼びます。
まとめ
- インターフェースを挟むことで、クラスを入れ替え可能になる