【設計入門2026 #19】インターフェースとDependency Injection

依存

モジュールを分割したら、次は個別にテストをすることを考えます。

以下の保存処理を考えましょう。

⚠️
入力されたユーザをDBに保存する
ただし、前章の条件を満たさない場合はエラーとする
前章の条件
🤝
結婚可能年齢に達している かつ 年収500万円以上

*結婚可能: 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種類の役割を持っているので、分割してみましょう。

Image in a image block
# アプリケーションメインロジック
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)を、ここではモジュール間の依存関係に適用して考えます。

参考: 📄Arrow icon of a page link【設計入門2026 #11】インターフェース / 📄Arrow icon of a page link【設計入門2026 #13】SOLID 原則

Image in a image block
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インターフェースを実装しているクラスなら何でも良いので、テスト時などでは適当なモックに差し替えて動かすことが可能です。

Image in a image block
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

まとめ

  • インターフェースを挟むことで、クラスを入れ替え可能になる