テストが書きにくいコードは設計が悪い?Pythonで学ぶクリーンアーキテクチャの本質

テストが書きにくいコードは設計が悪い?Pythonで学ぶクリーンアーキテクチャの本質 プログラミング

クリーンアーキテクチャという言葉を聞いたことがあるでしょうか。

Robert C. Martin(通称Uncle Bob)が提唱したこの設計原則は、多くの開発者に影響を与えてきました。
しかし、Pythonのような動的型付け言語でどう適用するのか。
この点で悩む方も多いはずです。

本記事では、海外のエンジニアコミュニティで話題となった議論を参考にします。
そして、Pythonにおけるクリーンアーキテクチャの実践的なアプローチについて解説していきます。

クリーンアーキテクチャは「全か無か」ではない

クリーンアーキテクチャを学び始めると、すべてのルールを厳密に守らなければならないと感じがちです。
しかし、実際にはそうではありません。

クリーンアーキテクチャは、開発者が自分のコンテキストに合わせて適用できる原則の集合体です。
重要なのは、その根底にある思想を理解すること。
これが出発点となります。

特にPythonは「私たちは皆、大人である」という哲学を持つ言語です。
厳格なルールの強制よりも、開発者の判断を尊重する文化が根付いています。

この特性とクリーンアーキテクチャの原則をどう調和させるか。
ここがPython開発者にとっての課題となるでしょう。

核心は依存関係管理とドメインモデリング

クリーンアーキテクチャの中で、長期的に価値を発揮するのは何でしょうか。
それは、依存関係管理と明確なドメイン境界の設計です。

以下のコードは、依存関係が混在した例です。

class OrderService:
    def create_order(self, user_id, items):
        # データベースに直接依存
        db = MySQLDatabase()
        user = db.query(f"SELECT * FROM users WHERE id = {user_id}")

        # 外部APIに直接依存
        payment = StripeAPI()
        payment.charge(user.card_id, self.calculate_total(items))

        # メール送信に直接依存
        email = SendGridClient()
        email.send(user.email, "Order confirmed")

このコードには問題があります。

OrderServiceがデータベース、決済API、メールサービスに直接依存しているのです。
そのため、テストが困難になります。

また、決済サービスを変更したい場合はどうでしょう。
OrderServiceのコードを修正しなければなりません。

依存関係を整理すると、次のようになります。

from abc import ABC, abstractmethod

class UserRepository(ABC):
    @abstractmethod
    def find_by_id(self, user_id: str) -> User:
        pass

class PaymentGateway(ABC):
    @abstractmethod
    def charge(self, card_id: str, amount: int) -> None:
        pass

class NotificationService(ABC):
    @abstractmethod
    def send_order_confirmation(self, email: str) -> None:
        pass

class OrderService:
    def __init__(
        self,
        user_repository: UserRepository,
        payment_gateway: PaymentGateway,
        notification_service: NotificationService
    ):
        self.user_repository = user_repository
        self.payment_gateway = payment_gateway
        self.notification_service = notification_service

    def create_order(self, user_id: str, items: list) -> Order:
        user = self.user_repository.find_by_id(user_id)
        total = self.calculate_total(items)

        self.payment_gateway.charge(user.card_id, total)
        self.notification_service.send_order_confirmation(user.email)

        return Order(user_id=user_id, items=items, total=total)

OrderServiceは抽象インターフェースにのみ依存するようになりました。
具体的な実装(MySQL、Stripe、SendGrid)はOrderServiceの外側で決定されます。

テスト容易性がアーキテクチャの価値を証明する

クリーンアーキテクチャの真価は、テストを書くときに実感できます。
依存関係を整理したOrderServiceは、以下のようにテスト可能です。

class FakeUserRepository(UserRepository):
    def __init__(self, users: dict):
        self.users = users

    def find_by_id(self, user_id: str) -> User:
        return self.users.get(user_id)

class FakePaymentGateway(PaymentGateway):
    def __init__(self):
        self.charges = []

    def charge(self, card_id: str, amount: int) -> None:
        self.charges.append({"card_id": card_id, "amount": amount})

class FakeNotificationService(NotificationService):
    def __init__(self):
        self.sent_emails = []

    def send_order_confirmation(self, email: str) -> None:
        self.sent_emails.append(email)

def test_create_order():
    # テスト用の偽実装を準備
    user_repo = FakeUserRepository({
        "user_1": User(id="user_1", email="test@example.com", card_id="card_123")
    })
    payment = FakePaymentGateway()
    notification = FakeNotificationService()

    service = OrderService(user_repo, payment, notification)

    # 注文を作成
    order = service.create_order("user_1", [{"item": "Book", "price": 1500}])

    # 検証
    assert order.total == 1500
    assert len(payment.charges) == 1
    assert payment.charges[0]["amount"] == 1500
    assert "test@example.com" in notification.sent_emails

実際のデータベースや外部APIに接続する必要はありません。
テストは高速に実行されます。

モックライブラリに頼らずとも、シンプルな偽実装でテストが書ける。
これは大きな利点です。

Functional Core, Imperative Shell という選択肢

クリーンアーキテクチャに関連する議論で、「Functional Core, Imperative Shell」というアプローチも注目されています。
このアプローチでは、ビジネスロジックを純粋関数として実装します。

そして、副作用(データベース操作、API呼び出しなど)を外殻に追い出すのです。

# Functional Core: 純粋なビジネスロジック
def calculate_order_total(items: list[Item], discount_rate: float) -> int:
    subtotal = sum(item.price * item.quantity for item in items)
    discount = int(subtotal * discount_rate)
    return subtotal - discount

def validate_order(user: User, items: list[Item]) -> list[str]:
    errors = []
    if not user.is_active:
        errors.append("User account is not active")
    if not items:
        errors.append("Order must contain at least one item")
    return errors

# Imperative Shell: 副作用を扱う外殻
class OrderController:
    def __init__(self, user_repo, order_repo):
        self.user_repo = user_repo
        self.order_repo = order_repo

    def handle_create_order(self, user_id: str, items: list[Item]):
        # 副作用: データベースから読み取り
        user = self.user_repo.find_by_id(user_id)

        # 純粋関数の呼び出し
        errors = validate_order(user, items)
        if errors:
            return {"success": False, "errors": errors}

        total = calculate_order_total(items, user.discount_rate)

        # 副作用: データベースに書き込み
        order = self.order_repo.save(Order(user_id, items, total))
        return {"success": True, "order_id": order.id}

純粋関数はテストが非常に簡単です。
入力を与えれば、常に同じ出力が返ってきます。

def test_calculate_order_total():
    items = [
        Item(name="Book", price=1000, quantity=2),
        Item(name="Pen", price=100, quantity=5)
    ]
    total = calculate_order_total(items, discount_rate=0.1)
    assert total == 2250  # (2000 + 500) * 0.9

クリーンアーキテクチャとFunctional Core, Imperative Shellは対立する概念ではありません。
むしろ、組み合わせて使うことで、より堅牢な設計が実現できるでしょう。

学習リソースについて

Pythonでクリーンアーキテクチャを学ぶ際、いくつかの書籍が評価されています。

「Architecture Patterns with Python」(通称Cosmic Python)は、ドメイン駆動設計やリポジトリパターンなど、実践的なパターンを解説した書籍です。

また、Sam Keenの「Clean Architecture with Python」も注目されています。
30年の開発経験に基づいた知見が詰まった一冊とのこと。

Cosmic Pythonで基礎を固めてから読むと、より理解が深まるという意見もあります。

まとめ

クリーンアーキテクチャは、厳密なルールの集合ではありません。

その本質は、依存関係を適切に管理し、ドメインを明確にモデリングすること。
ここにあります。

Pythonの実用主義的な文化と、クリーンアーキテクチャの原則は矛盾しません。
重要なのは、プロジェクトの規模や要件に応じて、適切なレベルで原則を適用することです。

テスト容易性という観点からアーキテクチャを評価すると、設計の良し悪しが明確になります。
テストが書きにくいと感じたら、それは依存関係を見直すサインかもしれません。

クリーンアーキテクチャの原則を学び、自分のプロジェクトに合った形で取り入れてみてください。
その過程で、コードの保守性とテスト容易性が向上していくはずです。

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