T-CREATOR

Python クリーンアーキテクチャ実践:依存逆転と境界インタフェースの具体化

Python クリーンアーキテクチャ実践:依存逆転と境界インタフェースの具体化

クリーンアーキテクチャの実践において、最も重要な原則の一つが「依存逆転の原則(DIP: Dependency Inversion Principle)」です。この原則を正しく適用することで、フレームワークやデータベース、外部サービスなどの詳細から独立したビジネスロジックを実現できます。

本記事では、Python における依存逆転の具体的な実装手法と、各層間の境界インタフェースをどのように設計すべきかを詳しく解説します。抽象的な理論ではなく、実際に動作するコードとともに、実践的なアーキテクチャ構築の方法をお伝えしますので、ぜひ最後までお読みください。

背景

クリーンアーキテクチャにおける依存の方向性

クリーンアーキテクチャは、ソフトウェアシステムを複数の層(レイヤー)に分割し、依存の方向を一方向に制限することで、保守性と拡張性を高める設計手法です。

従来のレイヤードアーキテクチャでは、上位層が下位層に依存する形でシステムが構築されます。しかし、クリーンアーキテクチャでは、中心にビジネスルール(Entities と Use Cases)を配置し、外側にあるインフラストラクチャ層が内側のビジネスルール層に依存する構造を採用しています。

以下の図は、クリーンアーキテクチャの基本的な層構造と依存の方向性を示しています。

mermaidflowchart TD
    subgraph outer["外側の層"]
        fw["フレームワーク<br/>Web/CLI"]
        adapter["アダプター層<br/>Controller/Presenter"]
        infra["インフラ層<br/>DB/外部API"]
    end

    subgraph inner["内側の層"]
        usecase["UseCase層<br/>アプリケーション<br/>ロジック"]
        entity["Entity層<br/>ビジネスルール"]
    end

    fw -->|"依存"| adapter
    adapter -->|"依存"| usecase
    infra -->|"依存"| usecase
    usecase -->|"依存"| entity

図で理解できる要点

  • 依存の方向は常に外側から内側へ
  • Entity 層は最も中心にあり、何にも依存しない
  • 外側の詳細(フレームワークや DB)は内側のビジネスルールに依存

この構造により、ビジネスロジックがフレームワークやデータベースの変更に影響を受けない設計が実現されます。

依存逆転の原則とは

依存逆転の原則(DIP)は、SOLID 原則の一つで、以下の 2 つの重要なルールで構成されています。

依存逆転の原則の定義

#ルール説明
1高レベルモジュールは低レベルモジュールに依存してはならないビジネスロジックがインフラの詳細に依存しない
2両者は抽象に依存すべきであるインタフェースや抽象クラスを介して依存関係を構築

通常のプログラミングでは、高レベルのビジネスロジックが、低レベルのデータベースアクセスや HTTP 通信などの具体的な実装に直接依存します。しかし、依存逆転の原則を適用すると、この依存の方向を「逆転」させることができるのです。

具体的には、ビジネスロジック層がインタフェース(抽象)を定義し、インフラ層がそのインタフェースを実装する形にします。これにより、ビジネスロジックは具体的な実装の詳細を知る必要がなくなります。

境界インタフェースの役割

境界インタフェース(Boundary Interface)は、異なる層の間に存在する抽象的な契約です。この境界を明確に定義することで、各層が独立して開発・テスト・変更できるようになります。

Python におけるクリーンアーキテクチャでは、以下の境界インタフェースが重要な役割を果たします。

主要な境界インタフェース

#インタフェース定義場所実装場所役割
1Repository InterfaceUseCase 層Infrastructure 層データ永続化の抽象化
2Use Case InterfaceUseCase 層UseCase 層ビジネスロジックの契約
3Presenter InterfaceUseCase 層Adapter 層出力データの整形
4Gateway InterfaceUseCase 層Infrastructure 層外部サービス連携

これらのインタフェースを適切に設計することで、各層の責務が明確になり、変更の影響範囲を最小限に抑えることができます。

以下の図は、境界インタフェースがどのように各層を分離しているかを示しています。

mermaidflowchart LR
    subgraph usecase_layer["UseCase層"]
        uc["UseCase<br/>ビジネス<br/>ロジック"]
        repo_if["Repository<br/>Interface"]
        gateway_if["Gateway<br/>Interface"]
    end

    subgraph infra_layer["Infrastructure層"]
        repo_impl["Repository<br/>実装"]
        gateway_impl["Gateway<br/>実装"]
        db[("Database")]
        api["外部API"]
    end

    uc -->|"使用"| repo_if
    uc -->|"使用"| gateway_if
    repo_impl -.->|"実装"| repo_if
    gateway_impl -.->|"実装"| gateway_if
    repo_impl --> db
    gateway_impl --> api

図で理解できる要点

  • インタフェースは UseCase 層に定義される
  • Infrastructure 層はインタフェースを実装する(依存の方向が逆転)
  • UseCase は具体的な実装を知らない

課題

伝統的なアーキテクチャの問題点

従来のレイヤードアーキテクチャでは、ビジネスロジックがデータベースアクセスやフレームワークの詳細に直接依存してしまう問題があります。

以下は、依存逆転を適用していない典型的なコード例です。

python# ❌ 依存逆転を適用していない例
class UserService:
    """ユーザーに関するビジネスロジック"""

    def __init__(self):
        # ビジネスロジック層が具体的なDB実装に直接依存
        self.db = MySQLConnection()

    def register_user(self, name: str, email: str):
        # SQLを直接記述している
        query = "INSERT INTO users (name, email) VALUES (?, ?)"
        self.db.execute(query, (name, email))
        return "User registered"

このコードには以下のような問題があります。

依存関係の問題点

#問題詳細影響
1具体実装への依存MySQLConnectionという具体クラスに直接依存DB の変更時にビジネスロジックを修正する必要がある
2テストの困難さモックやスタブへの置き換えが困難単体テストでも実際の DB が必要になる
3責務の混在ビジネスロジックとデータアクセスが混在コードの理解と保守が難しい
4変更の波及データベースの変更がビジネスロジックに影響小さな変更でも広範囲のコード修正が必要

Python における抽象化の課題

Python は動的型付け言語であり、Java や C# のような厳密なインタフェース機構を持っていません。そのため、抽象化の実装にはいくつかの選択肢があり、それぞれにメリットとデメリットがあります。

以下の表は、Python で利用可能な抽象化手法を比較したものです。

Python の抽象化手法比較

#手法型チェック明示性推奨度備考
1abc.ABC★★★★★★★★★最も推奨される標準的な方法
2Protocol★★★★★☆★★★構造的部分型、typing_extensions 必要
3ダックタイピング☆☆☆☆☆☆★☆☆型安全性が低い
4typing.Generic★★★★★★★★☆ジェネリクスと組み合わせて使用

多くの場合、abc.ABC(Abstract Base Class)を使用することで、明示的で型安全な抽象化を実現できます。しかし、適切に設計しないと、抽象化が不完全になり、依存逆転の恩恵を十分に受けられません。

境界の曖昧さによる問題

境界インタフェースが適切に定義されていないと、以下のような問題が発生します。

python# ❌ 境界が曖昧な例
class UserUseCase:
    """ユーザー登録のユースケース"""

    def __init__(self, repository):
        # repositoryの型が明示されていない
        self.repository = repository

    def execute(self, name: str, email: str):
        # repositoryがどんなメソッドを持つべきか不明
        user = self.repository.save_user(name, email)
        # 返り値の型も不明確
        return user

この例では、以下の問題があります。

境界の曖昧さによる問題

#問題点詳細
1契約の不明確さrepository がどんなメソッドを持つべきか明示されていない
2型情報の欠如引数や返り値の型が不明確で IDE の補完が効かない
3テスト時の混乱モックを作成する際に必要なメソッドが分からない
4実装者への負担インタフェースを実装する側が何を実装すべきか理解しづらい

以下の図は、境界が曖昧な場合の依存関係の問題を示しています。

mermaidflowchart TD
    uc["UserUseCase"]
    repo1["MySQLRepository"]
    repo2["PostgreSQLRepository"]
    repo3["MongoDBRepository"]

    uc -.->|"型不明<br/>何を期待?"| repo1
    uc -.->|"型不明<br/>何を期待?"| repo2
    uc -.->|"型不明<br/>何を期待?"| repo3

    style uc fill:#ffcccc
    style repo1 fill:#cccccc
    style repo2 fill:#cccccc
    style repo3 fill:#cccccc

図で理解できる要点

  • UseCase が具体的な実装に暗黙的に依存
  • インタフェースの契約が不明確
  • 実装の切り替えが困難

このような問題を解決するためには、明示的な型ヒントと抽象基底クラスを使用して、境界インタフェースを明確に定義する必要があります。

解決策

依存逆転の原則を適用する設計

依存逆転の原則を Python で実現するには、abcモジュールのABC(Abstract Base Class)とabstractmethodデコレータを使用します。これにより、インタフェースを明示的に定義し、実装を強制することができます。

以下の図は、依存逆転を適用した場合のアーキテクチャ構造を示しています。

mermaidflowchart TD
    subgraph domain["Domain層(内側)"]
        entity["User Entity<br/>ビジネスルール"]
    end

    subgraph usecase["UseCase層"]
        uc["RegisterUserUseCase"]
        repo_if["UserRepositoryInterface<br/>(抽象)"]
    end

    subgraph infra["Infrastructure層(外側)"]
        mysql_repo["MySQLUserRepository"]
        postgres_repo["PostgreSQLUserRepository"]
    end

    uc -->|"使用"| entity
    uc -->|"依存"| repo_if
    mysql_repo -.->|"実装"| repo_if
    postgres_repo -.->|"実装"| repo_if

    style repo_if fill:#ffffcc
    style entity fill:#ccffcc

図で理解できる要点

  • インタフェースは UseCase 層に配置される
  • Infrastructure 層がインタフェースを実装する(依存が逆転)
  • UseCase は抽象に依存し、具体実装を知らない

Repository パターンによる境界の定義

Repository パターンは、データアクセスロジックをビジネスロジックから分離するための重要なパターンです。クリーンアーキテクチャでは、この Repository のインタフェースを UseCase 層に配置することで、依存逆転を実現します。

まず、Repository インタフェースを定義します。

python# domain/repositories/user_repository.py
from abc import ABC, abstractmethod
from typing import Optional
from domain.entities.user import User

class UserRepositoryInterface(ABC):
    """
    ユーザーRepositoryの抽象インタフェース
    UseCase層に配置され、Infrastructure層から実装される
    """

    @abstractmethod
    def save(self, user: User) -> User:
        """
        ユーザーを永続化する

        Args:
            user: 保存するユーザーエンティティ

        Returns:
            保存されたユーザー(IDが付与される)
        """
        pass

このインタフェースは、以下の特徴を持ちます。

Repository インタフェースの設計原則

#原則理由実装例
1ドメインオブジェクトを扱うインフラの詳細を隠蔽Userエンティティを引数と返り値に使用
2永続化の詳細を含まないDB 種別に依存しないSQL や ORM の詳細は実装側に隠蔽
3ビジネス観点のメソッド名技術的な用語を避けるinsertではなくsave
4型ヒントを明示契約を明確化すべての引数と返り値に型を指定

次に、検索機能も追加します。

python# domain/repositories/user_repository.py(続き)

class UserRepositoryInterface(ABC):
    """ユーザーRepositoryの抽象インタフェース"""

    @abstractmethod
    def save(self, user: User) -> User:
        """ユーザーを永続化する"""
        pass

    @abstractmethod
    def find_by_id(self, user_id: str) -> Optional[User]:
        """
        IDでユーザーを検索する

        Args:
            user_id: 検索するユーザーのID

        Returns:
            見つかった場合はUserオブジェクト、見つからない場合はNone
        """
        pass

    @abstractmethod
    def find_by_email(self, email: str) -> Optional[User]:
        """
        メールアドレスでユーザーを検索する

        Args:
            email: 検索するメールアドレス

        Returns:
            見つかった場合はUserオブジェクト、見つからない場合はNone
        """
        pass

これで、ビジネスロジックが必要とするデータアクセスの契約が明確になりました。

UseCase インタフェースの設計

UseCase 層の境界を明確にするために、UseCase のインタフェースも定義します。これにより、Adapter 層(Controller)が UseCase に期待する契約が明確になります。

python# application/interfaces/use_case.py
from abc import ABC, abstractmethod
from typing import Generic, TypeVar

# 入力データの型変数
InputDTO = TypeVar('InputDTO')
# 出力データの型変数
OutputDTO = TypeVar('OutputDTO')

class UseCaseInterface(ABC, Generic[InputDTO, OutputDTO]):
    """
    全てのUseCaseが実装すべき基底インタフェース
    ジェネリクスを使用して入出力の型を明示する
    """

    @abstractmethod
    def execute(self, input_dto: InputDTO) -> OutputDTO:
        """
        ユースケースを実行する

        Args:
            input_dto: 入力データ転送オブジェクト

        Returns:
            output_dto: 出力データ転送オブジェクト
        """
        pass

このインタフェースにより、すべての UseCase が統一された契約を持つことになります。

UseCase インタフェースの設計ポイント

#ポイント説明メリット
1ジェネリクスの使用入出力の型を明示型安全性が向上
2DTO パターンデータ転送専用のオブジェクト層間のデータ受け渡しが明確
3単一メソッドexecute メソッドのみインタフェースの分離原則(ISP)に準拠
4抽象基底クラスABC 継承実装を強制できる

Presenter インタフェースの定義

出力の境界を定義するために、Presenter インタフェースも用意します。Presenter は、UseCase の実行結果を適切な形式(JSON、HTML、XML など)に変換する責務を持ちます。

python# application/interfaces/presenter.py
from abc import ABC, abstractmethod
from typing import Any, Generic, TypeVar

# 出力データの型変数
OutputDTO = TypeVar('OutputDTO')

class PresenterInterface(ABC, Generic[OutputDTO]):
    """
    出力データを整形するPresenterのインタフェース
    UseCaseからの出力を特定の形式に変換する責務を持つ
    """

    @abstractmethod
    def present(self, output_dto: OutputDTO) -> Any:
        """
        出力データを整形する

        Args:
            output_dto: UseCaseからの出力データ

        Returns:
            整形された出力(JSON、HTML、XML等)
        """
        pass

Presenter インタフェースを使用することで、ビジネスロジックと出力形式の変換を分離できます。

以下の図は、UseCase、Repository、Presenter の各インタフェースがどのように連携するかを示しています。

mermaidsequenceDiagram
    participant Controller
    participant UseCase
    participant RepoIF as Repository<br/>Interface
    participant Repo as Repository<br/>実装
    participant PresenterIF as Presenter<br/>Interface
    participant Presenter as Presenter<br/>実装

    Controller->>UseCase: execute(input_dto)
    UseCase->>RepoIF: find_by_email(email)
    RepoIF->>Repo: (実装を呼び出し)
    Repo-->>UseCase: User or None
    UseCase->>PresenterIF: present(output_dto)
    PresenterIF->>Presenter: (実装を呼び出し)
    Presenter-->>Controller: JSON/HTML等

図で理解できる要点

  • UseCase はインタフェースのみに依存
  • 実装の詳細は UseCase から隠蔽される
  • Controller は具体的な実装を注入する

依存性注入(DI)の実装

依存逆転を実現するためには、依存性注入(Dependency Injection)が不可欠です。Python では、コンストラクタ注入が最も一般的な手法です。

まず、シンプルな手動 DI の例を見てみましょう。

python# application/use_cases/register_user_use_case.py
from dataclasses import dataclass
from domain.repositories.user_repository import UserRepositoryInterface
from domain.entities.user import User

@dataclass
class RegisterUserInputDTO:
    """ユーザー登録の入力データ"""
    name: str
    email: str
    password: str

@dataclass
class RegisterUserOutputDTO:
    """ユーザー登録の出力データ"""
    user_id: str
    name: str
    email: str
    success: bool
    message: str

次に、UseCase の実装を見てみましょう。

python# application/use_cases/register_user_use_case.py(続き)
from application.interfaces.use_case import UseCaseInterface

class RegisterUserUseCase(
    UseCaseInterface[RegisterUserInputDTO, RegisterUserOutputDTO]
):
    """ユーザー登録のユースケース"""

    def __init__(self, user_repository: UserRepositoryInterface):
        """
        依存性注入によりRepositoryインタフェースを受け取る

        Args:
            user_repository: ユーザーRepositoryの実装(インタフェース型)
        """
        # 抽象インタフェースに依存する(具体実装を知らない)
        self._user_repository = user_repository

    def execute(
        self,
        input_dto: RegisterUserInputDTO
    ) -> RegisterUserOutputDTO:
        """ユーザー登録を実行する"""
        # メールアドレスの重複チェック
        existing_user = self._user_repository.find_by_email(
            input_dto.email
        )

        if existing_user:
            return RegisterUserOutputDTO(
                user_id="",
                name=input_dto.name,
                email=input_dto.email,
                success=False,
                message="このメールアドレスは既に登録されています"
            )

        # 新規ユーザーの作成
        new_user = User.create(
            name=input_dto.name,
            email=input_dto.email,
            password=input_dto.password
        )

        # 永続化
        saved_user = self._user_repository.save(new_user)

        return RegisterUserOutputDTO(
            user_id=saved_user.id,
            name=saved_user.name,
            email=saved_user.email,
            success=True,
            message="ユーザー登録が完了しました"
        )

このコードの重要なポイントは、__init__メソッドの引数型がUserRepositoryInterfaceになっている点です。これにより、UseCase は具体的な実装クラス(MySQLRepository など)を知ることなく、インタフェースにのみ依存できます。

依存性注入の設計ポイント

#ポイント実装方法メリット
1コンストラクタ注入__init__で依存を受け取る依存関係が明示的
2インタフェース型で宣言型ヒントに抽象型を使用依存の方向が正しい
3private フィールド_repositoryでカプセル化外部から変更されない
4不変性の維持注入後は変更しないスレッドセーフ

具体例

Entity 層の実装

まず、最も内側の層である Entity(エンティティ)を実装します。Entity は、ビジネスルールをカプセル化し、外部の詳細に一切依存しません。

python# domain/entities/user.py
from dataclasses import dataclass, field
from datetime import datetime
import uuid
import hashlib

@dataclass
class User:
    """
    ユーザーエンティティ
    ビジネスルールをカプセル化し、外部の詳細に依存しない
    """
    id: str
    name: str
    email: str
    password_hash: str
    created_at: datetime = field(default_factory=datetime.now)
    updated_at: datetime = field(default_factory=datetime.now)

    @staticmethod
    def create(name: str, email: str, password: str) -> 'User':
        """
        新しいユーザーを作成するファクトリメソッド

        Args:
            name: ユーザー名
            email: メールアドレス
            password: 平文のパスワード

        Returns:
            作成されたUserエンティティ
        """
        return User(
            id=str(uuid.uuid4()),
            name=name,
            email=email,
            password_hash=User._hash_password(password)
        )

パスワードのハッシュ化などのビジネスルールをエンティティ内に実装します。

python# domain/entities/user.py(続き)

    @staticmethod
    def _hash_password(password: str) -> str:
        """
        パスワードをハッシュ化する(ビジネスルール)

        Args:
            password: 平文のパスワード

        Returns:
            SHA256でハッシュ化されたパスワード
        """
        return hashlib.sha256(password.encode()).hexdigest()

    def verify_password(self, password: str) -> bool:
        """
        パスワードが正しいか検証する

        Args:
            password: 検証する平文のパスワード

        Returns:
            パスワードが正しい場合True
        """
        return self.password_hash == self._hash_password(password)

    def update_name(self, new_name: str) -> None:
        """
        ユーザー名を更新する

        Args:
            new_name: 新しいユーザー名
        """
        if not new_name or len(new_name.strip()) == 0:
            raise ValueError("ユーザー名は空にできません")

        self.name = new_name
        self.updated_at = datetime.now()

Entity には以下の特徴があります。

Entity 層の設計原則

#原則実装理由
1フレームワーク非依存標準ライブラリのみ使用どの環境でも利用可能
2ビジネスルール集約パスワードハッシュ化等ドメイン知識を一箇所に集約
3イミュータブル推奨dataclass 使用予期しない変更を防ぐ
4自己検証バリデーションを含む不正な状態を防ぐ

Infrastructure 層の Repository 実装

次に、外側の層である Infrastructure 層に Repository の具体実装を配置します。ここでは、UseCase で定義したインタフェースを実装します。

python# infrastructure/repositories/mysql_user_repository.py
from typing import Optional
import mysql.connector
from mysql.connector import MySQLConnection
from domain.repositories.user_repository import UserRepositoryInterface
from domain.entities.user import User

class MySQLUserRepository(UserRepositoryInterface):
    """
    MySQLを使用したUserRepositoryの実装
    UserRepositoryInterfaceを実装し、具体的なDB操作を行う
    """

    def __init__(self, connection: MySQLConnection):
        """
        MySQLコネクションを注入する

        Args:
            connection: MySQLデータベース接続
        """
        self._connection = connection

saveメソッドの実装を見てみましょう。

python# infrastructure/repositories/mysql_user_repository.py(続き)

    def save(self, user: User) -> User:
        """
        ユーザーをMySQLデータベースに保存する

        Args:
            user: 保存するユーザーエンティティ

        Returns:
            保存されたユーザーエンティティ
        """
        cursor = self._connection.cursor()

        # SQLクエリの構築(具体的なDB操作)
        query = """
            INSERT INTO users (
                id, name, email, password_hash, created_at, updated_at
            )
            VALUES (%s, %s, %s, %s, %s, %s)
        """

        values = (
            user.id,
            user.name,
            user.email,
            user.password_hash,
            user.created_at,
            user.updated_at
        )

        cursor.execute(query, values)
        self._connection.commit()
        cursor.close()

        return user

検索メソッドも実装します。

python# infrastructure/repositories/mysql_user_repository.py(続き)

    def find_by_id(self, user_id: str) -> Optional[User]:
        """
        IDでユーザーを検索する

        Args:
            user_id: 検索するユーザーID

        Returns:
            見つかった場合Userエンティティ、なければNone
        """
        cursor = self._connection.cursor(dictionary=True)

        query = "SELECT * FROM users WHERE id = %s"
        cursor.execute(query, (user_id,))

        row = cursor.fetchone()
        cursor.close()

        if row:
            return self._row_to_user(row)
        return None

    def find_by_email(self, email: str) -> Optional[User]:
        """
        メールアドレスでユーザーを検索する

        Args:
            email: 検索するメールアドレス

        Returns:
            見つかった場合Userエンティティ、なければNone
        """
        cursor = self._connection.cursor(dictionary=True)

        query = "SELECT * FROM users WHERE email = %s"
        cursor.execute(query, (email,))

        row = cursor.fetchone()
        cursor.close()

        if row:
            return self._row_to_user(row)
        return None

データベースの行をエンティティに変換するヘルパーメソッドも追加します。

python# infrastructure/repositories/mysql_user_repository.py(続き)

    def _row_to_user(self, row: dict) -> User:
        """
        データベースの行をUserエンティティに変換する

        Args:
            row: データベースから取得した行データ

        Returns:
            Userエンティティ
        """
        return User(
            id=row['id'],
            name=row['name'],
            email=row['email'],
            password_hash=row['password_hash'],
            created_at=row['created_at'],
            updated_at=row['updated_at']
        )

この Repository の実装により、以下のメリットが得られます。

Repository 実装のメリット

#メリット詳細
1データベース切替可能インタフェースを実装すれば、PostgreSQL 版も簡単に作成可能
2ビジネスロジック保護UseCase は SQL 文を一切知らない
3テスト容易性モック Repository を作成すれば DB なしでテスト可能
4変更の局所化DB 構造の変更は Repository 内で完結

Adapter 層の Controller 実装

Adapter 層の Controller は、外部からのリクエストを受け取り、UseCase を実行し、結果を返す役割を持ちます。ここでは、FastAPI を使った例を示します。

python# adapters/api/controllers/user_controller.py
from fastapi import APIRouter, HTTPException, Depends
from pydantic import BaseModel
from application.use_cases.register_user_use_case import (
    RegisterUserUseCase,
    RegisterUserInputDTO
)

# FastAPIのルーター
router = APIRouter(prefix="/users", tags=["users"])

class RegisterUserRequest(BaseModel):
    """
    ユーザー登録リクエストのスキーマ
    外部APIの形式を定義(Web層の関心事)
    """
    name: str
    email: str
    password: str

Controller で UseCase を使用します。

python# adapters/api/controllers/user_controller.py(続き)

@router.post("/register")
def register_user(
    request: RegisterUserRequest,
    use_case: RegisterUserUseCase = Depends(get_register_user_use_case)
):
    """
    ユーザー登録のエンドポイント

    Args:
        request: リクエストボディ
        use_case: 依存性注入されたUseCase

    Returns:
        ユーザー登録結果
    """
    # WebのリクエストをUseCaseのDTOに変換
    input_dto = RegisterUserInputDTO(
        name=request.name,
        email=request.email,
        password=request.password
    )

    # UseCaseを実行
    output_dto = use_case.execute(input_dto)

    # 結果をHTTPレスポンスに変換
    if not output_dto.success:
        raise HTTPException(status_code=400, detail=output_dto.message)

    return {
        "user_id": output_dto.user_id,
        "name": output_dto.name,
        "email": output_dto.email,
        "message": output_dto.message
    }

依存性注入の設定関数も定義します。

python# adapters/api/dependencies.py
from mysql.connector import connect
from infrastructure.repositories.mysql_user_repository import (
    MySQLUserRepository
)
from application.use_cases.register_user_use_case import (
    RegisterUserUseCase
)

def get_register_user_use_case() -> RegisterUserUseCase:
    """
    RegisterUserUseCaseを生成する依存性注入関数

    Returns:
        適切な依存関係が注入されたUseCase
    """
    # データベース接続を作成
    db_connection = connect(
        host="localhost",
        user="root",
        password="password",
        database="myapp"
    )

    # Repository実装を作成
    user_repository = MySQLUserRepository(db_connection)

    # UseCaseに注入して返す
    return RegisterUserUseCase(user_repository=user_repository)

Controller の責務は以下のように整理されます。

Controller 層の責務

#責務実装
1リクエスト変換Web 形式 →DTO 形式
2UseCase の実行ビジネスロジックの呼び出し
3レスポンス変換DTO 形式 →Web 形式
4エラーハンドリングHTTP ステータスコードの設定

テスト容易性の実証

依存逆転を適用したアーキテクチャでは、モックを使った単体テストが容易になります。以下は、データベースなしで UseCase をテストする例です。

python# tests/use_cases/test_register_user_use_case.py
import unittest
from unittest.mock import Mock
from application.use_cases.register_user_use_case import (
    RegisterUserUseCase,
    RegisterUserInputDTO
)
from domain.entities.user import User

class TestRegisterUserUseCase(unittest.TestCase):
    """RegisterUserUseCaseのテストクラス"""

    def setUp(self):
        """各テストの前に実行される準備処理"""
        # モックRepositoryを作成
        self.mock_repository = Mock()
        # UseCaseにモックを注入
        self.use_case = RegisterUserUseCase(
            user_repository=self.mock_repository
        )

正常系のテストを実装します。

python# tests/use_cases/test_register_user_use_case.py(続き)

    def test_register_user_success(self):
        """ユーザー登録が成功するケース"""
        # モックの振る舞いを設定(メールアドレスは未登録)
        self.mock_repository.find_by_email.return_value = None

        # saveメソッドが呼ばれたら、IDが付与されたUserを返す
        def save_side_effect(user):
            return user
        self.mock_repository.save.side_effect = save_side_effect

        # テスト対象を実行
        input_dto = RegisterUserInputDTO(
            name="山田太郎",
            email="yamada@example.com",
            password="securepass123"
        )
        output_dto = self.use_case.execute(input_dto)

        # 検証
        self.assertTrue(output_dto.success)
        self.assertEqual(output_dto.name, "山田太郎")
        self.assertEqual(output_dto.email, "yamada@example.com")
        self.mock_repository.save.assert_called_once()

異常系のテストも追加します。

python# tests/use_cases/test_register_user_use_case.py(続き)

    def test_register_user_email_already_exists(self):
        """メールアドレスが既に登録されている場合"""
        # 既存ユーザーをモックで返す
        existing_user = User(
            id="existing-id",
            name="既存ユーザー",
            email="yamada@example.com",
            password_hash="hash"
        )
        self.mock_repository.find_by_email.return_value = existing_user

        # テスト対象を実行
        input_dto = RegisterUserInputDTO(
            name="山田太郎",
            email="yamada@example.com",
            password="securepass123"
        )
        output_dto = self.use_case.execute(input_dto)

        # 検証
        self.assertFalse(output_dto.success)
        self.assertIn("既に登録されています", output_dto.message)
        # saveは呼ばれないはず
        self.mock_repository.save.assert_not_called()

このテストコードは、実際のデータベースを必要とせず、高速に実行できます。

テスト容易性のメリット

#メリット従来の方法依存逆転を適用した方法
1実行速度DB 接続で遅い(秒単位)モックで高速(ミリ秒単位)
2環境構築DB サーバーが必要コード内で完結
3データクリーンアップテスト後の削除が必要不要
4並列実行DB 競合で困難容易に並列化可能

DI コンテナの活用(応用編)

大規模なアプリケーションでは、手動での依存性注入が複雑になります。その場合、DI コンテナライブラリを使用すると管理が楽になります。

Python ではdependency-injectorなどのライブラリが利用できます。

python# infrastructure/container.py
from dependency_injector import containers, providers
from mysql.connector import connect
from infrastructure.repositories.mysql_user_repository import (
    MySQLUserRepository
)
from application.use_cases.register_user_use_case import (
    RegisterUserUseCase
)

class Container(containers.DeclarativeContainer):
    """
    依存性注入コンテナ
    アプリケーション全体の依存関係を一元管理
    """

    # 設定
    config = providers.Configuration()

    # データベース接続
    db_connection = providers.Singleton(
        connect,
        host=config.db.host,
        user=config.db.user,
        password=config.db.password,
        database=config.db.database
    )

    # Repository層
    user_repository = providers.Factory(
        MySQLUserRepository,
        connection=db_connection
    )

    # UseCase層
    register_user_use_case = providers.Factory(
        RegisterUserUseCase,
        user_repository=user_repository
    )

このコンテナを使用すると、依存関係の解決が自動化されます。

python# main.py
from infrastructure.container import Container

# コンテナの初期化
container = Container()
container.config.db.host.from_env("DB_HOST", "localhost")
container.config.db.user.from_env("DB_USER", "root")
container.config.db.password.from_env("DB_PASSWORD", "password")
container.config.db.database.from_env("DB_NAME", "myapp")

# UseCaseの取得(依存関係は自動で解決される)
use_case = container.register_user_use_case()

DI コンテナを使用することで、以下のメリットが得られます。

DI コンテナのメリット

#メリット詳細
1一元管理依存関係が一箇所に集約される
2ライフサイクル管理Singleton、Factory 等を簡単に設定
3設定の外部化環境変数や設定ファイルから注入
4テストの簡易化テスト用のコンテナを簡単に作成可能

以下の図は、DI コンテナを使用した場合の依存関係の解決フローを示しています。

mermaidflowchart TD
    start["アプリケーション起動"]
    container["DIコンテナ初期化"]
    config["設定読み込み"]
    db["DB接続生成"]
    repo["Repository生成"]
    usecase["UseCase生成"]
    controller["Controller起動"]

    start --> container
    container --> config
    config --> db
    db --> repo
    repo --> usecase
    usecase --> controller

    style container fill:#ffffcc
    style usecase fill:#ccffcc

図で理解できる要点

  • DI コンテナが依存関係を自動解決
  • 各層の生成順序が明確
  • 設定から実行まで一貫した流れ

まとめ

本記事では、Python におけるクリーンアーキテクチャの依存逆転の原則と境界インタフェースの具体的な実装方法を解説しました。

依存逆転の原則を適用することで、ビジネスロジックがフレームワークやデータベースなどの外部詳細から独立し、保守性と拡張性の高いシステムを構築できます。境界インタフェースを明確に定義することで、各層の責務が明確になり、チーム開発やテストが容易になるでしょう。

以下は、本記事で解説した重要なポイントのまとめです。

クリーンアーキテクチャ実践の要点

#要点実装技術効果
1依存の方向を内側へインタフェースを UseCase 層に配置ビジネスロジックの独立性
2Repository パターンabc.ABCで抽象化データアクセスの分離
3DTO パターンdataclass 使用層間データ転送の明確化
4依存性注入コンストラクタ注入テスト容易性の向上
5境界の明示型ヒント活用契約の明確化

実際のプロジェクトに適用する際は、まずシンプルな機能から始めて、徐々にアーキテクチャを洗練させていくことをお勧めします。最初から完璧を目指すのではなく、リファクタリングを通じて理想的な構造に近づけていくアプローチが効果的です。

Python の動的型付けという特性を活かしつつ、型ヒントと ABC を組み合わせることで、型安全で保守性の高いクリーンアーキテクチャを実現できます。本記事で紹介したパターンを参考に、ぜひ皆さんのプロジェクトでもクリーンアーキテクチャを実践してみてください。

関連リンク