Python | テスト・設計・品質:クリーンアーキテクチャ

Python Python
スポンサーリンク

クリーンアーキテクチャって何?一言でいうと「大事なものを真ん中に守る設計」

クリーンアーキテクチャは、アプリケーションの「大事なルール(ビジネスロジック)」を、外側の技術的なもの(Web フレームワーク、DB、外部サービスなど)から守るための設計の考え方です。
キーワードは「依存の向き」と「層(レイヤ)」です。

外側のものは、いつでも変わりうるものです。
フレームワークも DB も、数年後には別のものに変えたくなるかもしれません。
一方で、アプリの「本質的なルール」は、そう簡単には変わりません。

クリーンアーキテクチャは、「変わりやすいものが、変わりにくいものに依存するようにする」ことで、コアのロジックを長く守ろう、という発想です。


円のイメージでざっくりつかむクリーンアーキテクチャ

クリーンアーキテクチャは、よく「同心円」で説明されます。
中心に行くほど「ビジネスに近い・本質的なルール」、外側に行くほど「技術的・インフラ寄り」です。

ざっくり分けると、次のようなイメージになります。

中心に「エンティティ(ドメインルール)」
その外に「ユースケース(アプリケーションの振る舞い)」
さらに外に「インターフェース・アダプタ(DB や Web との橋渡し)」
一番外側に「フレームワーク・UI・DB・外部サービス」

重要なのは、「依存の向き」です。
外側は内側を知っていていいけれど、内側は外側を知らないようにする。
Python 的に言うと、「内側のコードは、外側のライブラリに import してはいけない」というイメージです。


具体例で見る:ユーザー登録機能をクリーンアーキテクチャで分解する

まずは「ごちゃ混ぜ版」のコード

よくある「フレームワークにべったり」な書き方を、あえてやってみます。

# app.py(フレームワークにべったりな例)

from flask import Flask, request, jsonify
import sqlite3

app = Flask(__name__)

@app.post("/users")
def register_user():
    data = request.json
    name = data.get("name")
    email = data.get("email")

    if not name:
        return jsonify({"error": "name is required"}), 400
    if "@" not in email:
        return jsonify({"error": "invalid email"}), 400

    conn = sqlite3.connect("app.db")
    cur = conn.cursor()
    cur.execute(
        "INSERT INTO users (name, email) VALUES (?, ?)",
        (name, email),
    )
    conn.commit()

    return jsonify({"message": "ok"}), 201
Python

HTTP のこと(Flask)
DB のこと(sqlite3)
ビジネスルール(名前必須、メール形式チェック)

が全部一つの関数に詰まっています。

動くことは動きますが、次のような問題が出てきます。

フレームワークを変えたくなったら全部書き換え
DB を変えたくなっても全部書き換え
ビジネスルールだけをテストするのが難しい

ここでクリーンアーキテクチャの考え方を使って、分解していきます。

ドメイン(中心):ユーザーのルールを表す

まずは「ユーザー」という概念と、そのルールだけを切り出します。

# domain/user.py

from dataclasses import dataclass

@dataclass
class User:
    name: str
    email: str

    def validate(self) -> None:
        if not self.name:
            raise ValueError("name is required")
        if "@" not in self.email:
            raise ValueError("invalid email")
Python

ここには、Flask も sqlite3 も出てきません。
「ユーザーとは何か」「どんなときにエラーとするか」だけが書かれています。

これが「内側の層」です。
ここは、できるだけ技術から切り離しておきたい場所です。

ユースケース層:アプリとして「何をするか」を表す

次に、「ユーザーを登録する」というユースケースを表現します。

# usecase/register_user.py

from dataclasses import dataclass
from typing import Protocol
from domain.user import User

class UserRepository(Protocol):
    def save(self, user: User) -> None:
        ...

@dataclass
class RegisterUserInput:
    name: str
    email: str

@dataclass
class RegisterUserOutput:
    message: str


def register_user(
    repo: UserRepository,
    data: RegisterUserInput,
) -> RegisterUserOutput:
    user = User(name=data.name, email=data.email)
    user.validate()
    repo.save(user)
    return RegisterUserOutput(message="ok")
Python

ここで重要なのは、DB の具体的な実装を知らないことです。
UserRepository という Protocol(インターフェース)に依存していて、
「save できる何か」があれば動くようになっています。

HTTP のことも知りません。
「入力データを受け取り、ユーザーを検証して保存し、結果を返す」
というユースケースだけを担当しています。

インフラ層:DB の具体的な実装を書く

DB にユーザーを保存する実装は、外側に追い出します。

# infrastructure/sqlite_user_repository.py

import sqlite3
from domain.user import User
from usecase.register_user import UserRepository

class SqliteUserRepository(UserRepository):
    def __init__(self, db_path: str) -> None:
        self._db_path = db_path

    def save(self, user: User) -> None:
        conn = sqlite3.connect(self._db_path)
        cur = conn.cursor()
        cur.execute(
            "INSERT INTO users (name, email) VALUES (?, ?)",
            (user.name, user.email),
        )
        conn.commit()
        conn.close()
Python

ここは sqlite3 にべったりで構いません。
でも、このコードは「ユースケース側」からは見えません。
ユースケースは UserRepository という抽象にしか依存していないからです。

プレゼンテーション層:HTTP とユースケースをつなぐ

最後に、Flask のコードは「ユースケースを呼び出すだけ」にします。

# interface/flask_app.py

from flask import Flask, request, jsonify
from usecase.register_user import (
    register_user,
    RegisterUserInput,
)
from infrastructure.sqlite_user_repository import SqliteUserRepository

app = Flask(__name__)
repo = SqliteUserRepository("app.db")

@app.post("/users")
def register_user_endpoint():
    data = request.json or {}
    input_data = RegisterUserInput(
        name=data.get("name", ""),
        email=data.get("email", ""),
    )
    try:
        output = register_user(repo, input_data)
        return jsonify({"message": output.message}), 201
    except ValueError as e:
        return jsonify({"error": str(e)}), 400
Python

ここは HTTP のことだけを考えます。
JSON から入力を取り出し、ユースケースに渡し、結果を HTTP レスポンスに変換する。
ビジネスルールは一切書きません。

こうしてみると、責務がきれいに分かれたのが分かるはずです。


依存性逆転の考え方をもう少しだけ深掘りする

クリーンアーキテクチャの核にあるのが「依存性逆転」です。
普通に書くと、「ビジネスロジックが DB やフレームワークに依存する」形になりがちです。

さっきの最初の例では、register_user が sqlite3 に直接依存していました。
これは「内側が外側に依存している」状態です。

クリーンアーキテクチャでは、これをひっくり返します。
「外側が内側に依存する」ようにします。

ユースケースは UserRepository という抽象に依存する
具体的な SqliteUserRepository は、その抽象を実装する

このとき、依存の矢印はこうなります。

SqliteUserRepositoryUserRepositoryregister_userUser

外側から内側に向かって矢印が伸びているイメージです。
内側は、外側の存在を知りません。

この「内側は外側を知らない」という状態が、
テストのしやすさと変更のしやすさを生みます。


クリーンアーキテクチャとテスト・品質の関係

クリーンアーキテクチャは、「テストしやすさ」をかなり意識した設計です。
さっきの構成だと、次のようなテストが書きやすくなります。

ドメイン層(User.validate)のテスト
ユースケース層(register_user)のテスト(リポジトリをテスト用のダミーに差し替える)
インフラ層(SqliteUserRepository)のテスト(必要なら別途)
HTTP 層(Flask)は最小限だけテストする

例えば、ユースケースだけをテストするなら、こんな感じになります。

from usecase.register_user import (
    register_user,
    RegisterUserInput,
    UserRepository,
)
from domain.user import User

class DummyRepo(UserRepository):
    def __init__(self) -> None:
        self.saved: list[User] = []

    def save(self, user: User) -> None:
        self.saved.append(user)


def test_register_user_success():
    repo = DummyRepo()
    input_data = RegisterUserInput(name="Taro", email="taro@example.com")

    output = register_user(repo, input_data)

    assert output.message == "ok"
    assert len(repo.saved) == 1
    assert repo.saved[0].email == "taro@example.com"
Python

ここでは Flask も sqlite3 も出てきません。
「ユースケースのロジック」だけを、純粋にテストできます。

これが、クリーンアーキテクチャが品質に効く一番大きな理由です。
「ビジネスロジックを、外部の技術から切り離してテストできる」状態を作るからです。


初心者がクリーンアーキテクチャとどう付き合うといいか

いきなり完璧なクリーンアーキテクチャを目指すと、ほぼ確実に挫折します。
特に小さなスクリプトや個人開発では、「やりすぎ」にもなりがちです。

最初の一歩としては、次のような意識だけでも十分価値があります。

ビジネスロジック(ルール)を、フレームワークや DB から分けてみる
「ユースケース関数」を一つ作って、そこにロジックを集める
DB や外部サービスへのアクセスは、「リポジトリ的なクラス」に追い出す
ユースケース関数を、フレームワーク抜きでテストしてみる

つまり、「層を4つきっちり作る」よりも先に、
「ビジネスロジックを外側から分離する」ことを意識するのが大事です。

クリーンアーキテクチャは「型にはめるための宗教」ではなく、
「変わりにくいものを守り、変わりやすいものを外側に追い出すための考え方」です。
そのエッセンスだけでも取り入れると、コードの寿命と品質がかなり変わります。


まとめ(クリーンアーキテクチャは「依存の向きを整えて、コアを守る設計」)

クリーンアーキテクチャを初心者目線でまとめると、こうなります。

アプリの「本質的なルール(ドメイン・ユースケース)」を中心に置き、フレームワーク・DB・外部サービスなどの「技術的なもの」を外側に追い出し、外側が内側に依存するようにする設計思想である。
依存性逆転によって、ビジネスロジックが具体的な技術に縛られなくなり、テストしやすく、変更に強い構造になる。
完璧な円やレイヤーを最初から目指す必要はなく、「ビジネスロジックをフレームワークや DB から分離する」「ユースケース関数を作る」といった小さな一歩からでも、十分に効果が出る。

タイトルとURLをコピーしました