安全に壊れるための設計原則
安全に壊れるための設計原則
Section titled “安全に壊れるための設計原則”「正常に動く」よりも「異常時に安全に壊れる」ことを優先する設計原則を詳しく解説します。
境界防御 (Boundary Defense)
Section titled “境界防御 (Boundary Defense)”外部(API・DB・ユーザ入力)からのデータは常に汚染されていると仮定し、型・形式・範囲を検査してからロジックに渡す。
# ❌ 悪い例: 無防備な入力受付from fastapi import FastAPI
app = FastAPI()
@app.post("/users")async def create_user(data: dict): # 問題: 型チェックなし、バリデーションなし name = data.get("name") age = data.get("age") user = create_user_in_db(name, age) return user
# ✅ 良い例: 境界防御の実装from pydantic import BaseModel, EmailStr, Field, validator
# Pydanticで境界を定義class CreateUserRequest(BaseModel): name: str = Field(..., min_length=1, max_length=100) age: int = Field(..., ge=0, le=150) email: EmailStr
@validator('name') def validate_name(cls, v): if not v.strip(): raise ValueError('Name cannot be empty') return v.strip()
@app.post("/users")async def create_user(request: CreateUserRequest): # バリデーション: Pydanticで型・形式・範囲を検査 user = await create_user_in_db(request.name, request.age, request.email) return userなぜ重要か:
- 型安全性: Pydanticで実行時に型チェック
- バリデーション: 形式・範囲を検査
- セキュリティ: SQLインジェクション、XSSなどの攻撃を防止
副作用の局所化
Section titled “副作用の局所化”DB更新・通知・外部呼出などの副作用をロジックの末尾に集約し、それ以前を状態を持たない純粋処理として保つ。
# ❌ 悪い例: 副作用が散在async def create_order(order_data: OrderData) -> Order: # 副作用1: DB更新 order = await order_repo.save(order_data)
# ビジネスロジック(副作用が混在) if order.amount > 10000: # 副作用2: 外部API呼び出し await notification_service.send_email(order.user_id)
# 副作用3: 別のDB更新 await audit_log_repo.save("ORDER_CREATED", order.id)
return order
# ✅ 良い例: 副作用の局所化async def create_order(order_data: OrderData) -> Order: # 1. 純粋処理: ビジネスロジック(副作用なし) order = validate_and_create_order(order_data)
# 2. 副作用の集約: すべての副作用を末尾に await persist_order(order) await notify_if_needed(order) await audit_order_creation(order)
return order
# 純粋関数: 副作用なしdef validate_and_create_order(order_data: OrderData) -> Order: # バリデーションとオブジェクト作成のみ if order_data.amount <= 0: raise ValueError("Invalid amount") return Order( id=generate_id(), amount=order_data.amount, )
# 副作用: DB更新async def persist_order(order: Order) -> None: await order_repo.save(order)
# 副作用: 通知async def notify_if_needed(order: Order) -> None: if order.amount > 10000: await notification_service.send_email(order.user_id)
# 副作用: 監査ログasync def audit_order_creation(order: Order) -> None: await audit_log_repo.save("ORDER_CREATED", order.id)なぜ重要か:
- テスト容易性: 純粋関数は単体テストが容易
- 可読性: 副作用が明確に分離される
- デバッグ容易性: 副作用の発生箇所が明確
ビジネスロジックが特定ライブラリやORMの仕様に依存しないよう、インターフェース層で抽象化する。
# ❌ 悪い例: ORMに直接依存class OrderService: async def find_order(self, order_id: int) -> Order: # SQLAlchemyの仕様に依存 return await db.query(Order).filter(Order.id == order_id).first()
# ✅ 良い例: インターフェースで抽象化from abc import ABC, abstractmethod
# ドメイン層のインターフェースclass OrderRepository(ABC): @abstractmethod async def find_by_id(self, order_id: int) -> Order | None: pass
@abstractmethod async def save(self, order: Order) -> Order: pass
# インフラ層の実装class SQLAlchemyOrderRepository(OrderRepository): def __init__(self, db: Session): self.db = db
async def find_by_id(self, order_id: int) -> Order | None: result = await self.db.query(Order).filter(Order.id == order_id).first() return result
async def save(self, order: Order) -> Order: self.db.add(order) await self.db.commit() return order
# サービス層: ドメイン層のインターフェースに依存class OrderService: def __init__(self, repository: OrderRepository): self.repository = repository
async def find_order(self, order_id: int) -> Order: order = await self.repository.find_by_id(order_id) if not order: raise OrderNotFoundError(order_id) return orderなぜ重要か:
- 交換容易性: ORMを変更してもビジネスロジックは変更不要
- テスト容易性: モックで簡単にテスト可能
- 保守性: フレームワークの変更に強い
安全に壊れるための設計原則のポイント:
- 境界防御: 外部データは常に汚染されていると仮定し、型・形式・範囲を検査
- 副作用の局所化: 副作用をロジックの末尾に集約し、純粋処理と分離
- 依存の隔離: ビジネスロジックが特定ライブラリに依存しないよう抽象化
これらの原則により、「異常時に安全に壊れる」堅牢なシステムを構築できます。