ここ数日でクリーンアーキテクチャについて学んだのでコードでまとめる

1. はじめに

クリーンアーキテクチャは“実物”で理解するのが一番早い

抽象的な説明だとイマイチわからなかったため、本記事は「動くコード」を通して理解することを目的としています。

「商品価格を取得する簡易アプリ」を例として扱っていきます。

2. クリーンアーキテクチャとは

クリーンアーキテクチャは

  • ビジネスロジック(ドメイン)を中心に置く
  • 外側の層に依存しないようにする
  • 内側 → 外側の依存は禁止。外側 → 内側はOK。

という“依存方向”のルールがあるアーキテクチャです。

特に重要なのはこれだけ:

UI や DB が変わっても、ビジネスロジックは壊れないようにする

🚨 なぜ、わざわざ面倒な分離をするのか? (アンチパターン)

コードを分離しない場合、以下のような「密結合」が起きがちです。

# 理想: シンプルなビジネスルールだけ
class Item:
    def get_price_from_db(self):
        # ❌ DBアクセスコードがここに混入!
        # MySQL接続ライブラリを使ってSELECT文を発行...
        pass 

もしItemクラスの中にDBアクセスコードが書かれていたら、以下の問題に直面します。

  • テストの困難さ: Itemクラスのテストをするたびに、実際にDBに接続しなければならない。
  • 変更のコスト: DBをMySQLからPostgreSQLに変えたくなったとき、ビジネスロジックの核であるItemクラスまで修正しなければならない。

クリーンアーキテクチャは、この「外部の事情(DB, UI, フレームワーク)」から「ビジネスの核」を隔離し、変更に強い設計を目指すための設計原則なのです。

3. サンプルアプリの説明

今回作るのは以下:

  • Controllerが「商品ID」を受け取る
  • UseCaseが価格を取得する処理を行う
  • RepositoryがDBから商品情報を取得する
  • Domain(Entity)がビジネスルールを持つ

4. Domain(エンティティ) — ビジネスロジックの中心

まずは最も内側の Domain(Entity)

  • DBにもUIにも依存しない
  • 純粋なビジネスルールだけ書く
# domain/item.py
from dataclasses import dataclass

@dataclass
class Item:
    id: int
    price: int

    def validate_price(self):
        """価格に関するビジネスルール"""
        if self.price < 0:
            raise ValueError("Price must be >= 0")
        
    def calculate_tax(self, rate: float = 0.1):
        """価格計算に関するロジック"""
        return int(self.price * rate)

ポイント:

  • Item はシンプルなモデル
  • 価格が負数ならエラーにするなど、ビジネスルールはここに置く
  • DBの型やSQL、Webフレームワークの事情は絶対に入れないのが重要です。

5. UseCase(Application)— アプリケーションの流れを定義する

UseCase は「アプリの目的」をコードで表現する層であり、各層の中で最も重要な役割を果たします。

🔑 依存性逆転の原則 (DIP) とインターフェース

UseCaseがRepository(DBアクセス)を利用したい場合、具体的な実装(MySQLなど)に依存してはいけません。そこで、まず「契約」であるインターフェース(抽象クラス)を定義します。

# usecase/repository.py
from abc import ABC, abstractmethod
from domain.item import Item # Domainへの依存はOK (内側)

class ItemRepository(ABC):
    """
    これは「Itemを取得する機能」の契約(インターフェース)です。
    DB実装の詳細を知りません。
    """
    @abstractmethod
    def find_by_id(self, item_id: int) -> Item | None:
        pass

次に、UseCase本体。

# usecase/get_item_price.py
from dataclasses import dataclass
from usecase.repository import ItemRepository # 契約への依存はOK
# from infrastructure.item_repository_mysql import ItemRepositoryMySQL # 🚨 やってはいけない!

@dataclass
class GetItemPriceInput:
    item_id: int

@dataclass
class GetItemPriceOutput:
    item_id: int
    price: int
    tax: int # 項目を追加

class GetItemPriceUseCase:
    def __init__(self, repository: ItemRepository):
        # ここでItemRepositoryの「契約」を受け取る。
        # MySQLの実装なのか、メモリ上の実装なのかは知らない。
        self.repository = repository

    def execute(self, input_data: GetItemPriceInput) -> GetItemPriceOutput:
        item = self.repository.find_by_id(input_data.item_id)

        if item is None:
            raise ValueError("Item not found")

        # Domainのロジックを使用
        item.validate_price() 
        calculated_tax = item.calculate_tax()

        return GetItemPriceOutput(
            item_id=item.id,
            price=item.price,
            tax=calculated_tax
        )

ポイント:

  • UseCaseは、具体的なDB実装を知らず、ItemRepositoryというインターフェースだけに依存しています。
  • これが「依存性逆転の原則(DIP)」です。UseCaseはビジネスの流れを定義する層であり、外部の技術に左右されてはいけないため、依存を内側に向けます。

5. Infrastructure — DBアクセスなど外部の世界

ここでは、UseCaseで定義したItemRepositoryのインターフェースを、具体的な外部技術(MySQLなど)で実装します。

# infrastructure/item_repository_mysql.py
from usecase.repository import ItemRepository # 外側が内側の契約に依存
from domain.item import Item

class ItemRepositoryMySQL(ItemRepository):
    """
    ItemRepositoryインターフェースの実装。
    MySQL接続、SQL発行など、外部とのやり取りをすべてここに閉じ込める。
    """
    def find_by_id(self, item_id: int) -> Item | None:
        # 本来はMySQLへの接続やSQL発行を書く
        # 今回はサンプルとして疑似的に返す
        data = {
            1: 1000,
            2: 500,
        }
        price = data.get(item_id)

        if price is None:
            return None

        # 取得したデータをDomainのItemに戻す
        return Item(id=item_id, price=price)

ポイント:

  • SQLやDB接続などの外部の世界はここに閉じ込めます。
  • もしDBをPostgreSQLに変える場合、ItemRepositoryPostgreSQLというクラスを新しく作り、この層だけを変更すれば済みます。UseCaseとDomainは不変です。

6. Interface Adapter(Controller)— 入出力の変換

Webのリクエストを受けてUseCaseを呼び出し、レスポンスを整形する層です。

# controller/item_controller.py
from usecase.get_item_price import (
    GetItemPriceUseCase,
    GetItemPriceInput
)
from infrastructure.item_repository_mysql import ItemRepositoryMySQL 
# Infrastructureへの依存はOK (外側)

class ItemController:
    def get_item_price(self, item_id: int):
        # 依存関係の注入(DI)
        # この層で、使うRepositoryの実装(MySQL)を指定し、UseCaseに渡す
        repo = ItemRepositoryMySQL() 
        usecase = GetItemPriceUseCase(repo)

        # 1. 入力データの変換
        input_data = GetItemPriceInput(item_id=item_id)
        
        # 2. UseCaseの実行
        output = usecase.execute(input_data)

        # 3. 出力データの変換 (JSONやHTTPレスポンス形式へ)
        return {
            "itemId": output.item_id,
            "price": output.price,
            "tax": output.tax,
            "message": "Success"
        }

ポイント:

  • Controllerは、UseCaseを呼ぶだけです。
  • ここで、UI/Webフレームワーク特有の処理(リクエストの解析、JSONの生成など)を行います。

7. 全体の流れ(コードで追う)

GET /item/1 的なリクエストが来たら、コードは以下の流れを辿ります。

  1. Controller: itemIdを受け取り、ItemRepositoryMySQLUseCaseに渡す
  1. UseCase: repository.find_by_id()を呼び出す(契約)。
  2. Infrastructure (MySQL): 実際にDBに接続し、データを取得。Itemインスタンスを生成して返す。
  3. UseCase: 取得したItemのビジネスルール(validate_price, calculate_tax)をチェック・実行。
  4. Controller: UseCaseから受け取ったOutputDataをJSONに成形してレスポンス。

どの層も役割が明確であり、依存の方向は常に内側(Domain)に向かって流れていることが確認できます。

8. メリット

クリーンアーキテクチャの導入は、初期のコード量は増えますが、それは未来への投資です。

🛡️ メリット

  • テストの容易性: UseCaseDomainのビジネスロジックは、外部の要素(DBなど)と完全に分離されているため、インメモリのリポジトリ(モック)を使って高速かつ安定してテストできます。
  • 技術の切り替え: 「データベースを変えたい」「UIをWebからCLI(コマンドライン)に変えたい」といった技術的な要望が出たとき、InfrastructureInterface Adapter層だけを修正すれば済みます。ビジネスの核は手つかずで済みます。

この設計を実践することで、変更に強い、柔軟なソフトウェアに一歩近づけます。