Skip to content

安全に壊れるための設計原則

「正常に動く」よりも「異常時に安全に壊れる」ことを優先する設計原則を詳しく解説します。

外部(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などの攻撃を防止

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を変更してもビジネスロジックは変更不要
  • テスト容易性: モックで簡単にテスト可能
  • 保守性: フレームワークの変更に強い

安全に壊れるための設計原則のポイント:

  • 境界防御: 外部データは常に汚染されていると仮定し、型・形式・範囲を検査
  • 副作用の局所化: 副作用をロジックの末尾に集約し、純粋処理と分離
  • 依存の隔離: ビジネスロジックが特定ライブラリに依存しないよう抽象化

これらの原則により、「異常時に安全に壊れる」堅牢なシステムを構築できます。