依存
モジュールを分割したら、次は個別にテストをすることを考えます。
以下の保存処理を考えましょう。
ただし、前章の条件を満たさない場合はエラーとする
前章の条件
*結婚可能: 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:
self.__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
type Sex = "man" | "woman";
type Age = { value: number };
type Income = { value: number };
type User = {
name: string;
sex: Sex;
age: Age;
income: Income;
};
class Application {
// 適当なDBクライアントを想定
private dbClient: DBClient;
constructor() {
this.dbClient = new DBClient(...);
}
createUser(user: User): void {
if (this.isAcceptable(user)) {
this.dbClient.exec("INSERT INTO user VALUES(?, ?, ?, ?)", [
user.name,
user.sex,
user.age.value,
user.income.value,
]);
} else {
throw new Error("User is not acceptable for this service.");
}
}
// 前章と同一ロジック
private isAcceptable(user: User): boolean {
if (
(user.sex === "man" && user.age.value >= 18) ||
(user.sex === "woman" && 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 Application {
private repository: UserRepository;
constructor() {
this.repository = new UserRepository();
}
createUser(user: User): void {
if (this.isAcceptable(user)) {
this.repository.save(user);
} else {
throw new Error("User is not acceptable for this service.");
}
}
private isAcceptable(user: User): boolean {
return (
(user.sex === "man" && user.age.value >= 18) ||
(user.sex === "woman" && user.age.value >= 16)
) && user.income.value >= 5;
}
}
# ユーザデータに責務を持つ
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)) // ユーザデータに責務を持つ
class UserRepository {
// 適当なDBクライアントを想定
private dbClient: DBClient;
constructor() {
this.dbClient = new DBClient(...);
}
save(user: User): void {
this.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)を、ここではモジュール間の依存関係に適用して考えます。
参考: 📄【設計入門2026 #11】インターフェース / 📄
【設計入門2026 #13】SOLID 原則
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 class Application {
private repository: IUserRepository;
// コンストラクタで挿入
constructor(repository: IUserRepository) {
this.repository = repository;
}
createUser(user: User): void {
if (this.isAcceptable(user)) {
this.repository.save(user);
} else {
throw new Error("User is not acceptable for this service.");
}
}
private isAcceptable(user: User): boolean {
return (
(user.sex === "man" && user.age.value >= 18) ||
(user.sex === "woman" && user.age.value >= 16)
) && user.income.value >= 5;
}
} from abc import ABC, abstractmethod
# インターフェース
class IUserRepository(ABC):
# save()メソッドがあることだけを宣言
@abstractmethod
def save(self, user: User):
raise NotImplementedError() // インターフェース
interface IUserRepository {
// save()メソッドがあることだけを宣言
save(user: User): void;
} # インターフェースを実装していることを宣言
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)) // インターフェースを実装していることを宣言
class UserRepository implements IUserRepository {
// 適当なDBクライアントを想定
private dbClient: DBClient;
constructor() {
this.dbClient = new DBClient(...);
}
save(user: User): void {
this.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 class MockUserRepository implements IUserRepository {
// DBの代わりのMap
private mockMap = new Map<string, User>();
save(user: User): void {
this.mockMap.set(user.name, user);
}
} このように、直接クラスを参照せずにインターフェースを参照することで、実体クラスは入れ替え可能になります。
これはテストの差し替えだけでなく、
- DBクライアントライブラリの仕様が変わった
- DBを変えなければいけなくなった
など、実装変更が必要になった場合、一部クラスの差し替えだけで済む(=影響範囲が限定される)ことを意味します。
依存性の注入(Dependency Injection)
インターフェースを導入したことで、実体クラスインスタンスを渡す必要が出ました。
これは通常、main関数などの最上位の関数で行います。
if __name__ == "main":
# IUserRepotitoryインターフェースを満たすクラスを用意して
repository = UserRepository()
# 依存として渡す(=注入)
application = Application(repository)
application.createUser(...)
function main() {
// IUserRepositoryインターフェースを満たすクラスを用意して
const repository = new UserRepository();
// 依存として渡す(=注入)
const application = new Application(repository);
application.createUser(...);
} このように、依存するインスタンス(オブジェクト)を外から与えることを依存性の注入(Dependency Injection = DI)といい、特にコンストラクタで与えることをコンストラクタインジェクションと呼びます。
DIは依存クラスが増えてくるとだんだん辛くなってきます。
if __name__ == "main":
# 設定値管理クラスを作成
config = Config()
# ロガーを作成
logger = Logger(config)
# DBコネクション管理用クラスを作る
connection = DBConnection(config)
# UserRepositoryを作る
userRepository = UserRepository(config, logger, connection)
# TaskRepositoryを作る
taskRepository = TaskRepository(config, logger, connection)
# アプリケーション作成
application = Application(config, logger, userRepository, taskRepository)
application.createUser(...)
applocation.createTask(...)
function main() {
// 設定値管理クラスを作成
const config = new Config();
// ロガーを作成
const logger = new Logger(config);
// DBコネクション管理用クラスを作る
const connection = new DBConnection(config);
// UserRepositoryを作る
const userRepository = new UserRepository(config, logger, connection);
// TaskRepositoryを作る
const taskRepository = new TaskRepository(config, logger, connection);
// アプリケーション作成
const application = new Application(
config,
logger,
userRepository,
taskRepository
);
application.createUser(...);
application.createTask(...);
} これ楽にするため、自動的に依存関係を解析してくれるライブラリに任せることがあります。
このようなライブラリをDIコンテナと呼びます。
動作はライブラリや言語によって様々ですが、
- コンストラクタの型情報を読む
- 専用のアノテーションを付ける
- 専用のファイルで依存関係を指定する
などの方法で依存関係を取得・解析し、生成順序を決定してコンストラクタを呼び出すようになっています。
graph TD
Application --> Config
Application --> Logger
Application --> UserRepository
Application --> TaskRepository
UserRepository --> Config
UserRepository --> Logger
UserRepository --> DBConnection
TaskRepository --> Config
TaskRepository --> Logger
TaskRepository --> DBConnection
DBConnection --> Config
Logger --> Config まとめ
- インターフェースを挟むことで、クラスを入れ替え可能になる