安全に壊れるための設計原則
安全に壊れるための設計原則
Section titled “安全に壊れるための設計原則”「正常に動く」よりも「異常時に安全に壊れる」ことを優先する設計原則を詳しく解説します。
境界防御 (Boundary Defense)
Section titled “境界防御 (Boundary Defense)”外部(API・DB・ユーザ入力)からのデータは常に汚染されていると仮定し、型・形式・範囲を検査してからロジックに渡す。
// ❌ 悪い例: 無防備な入力受付app.post('/users', (req, res) => { // 問題: 型チェックなし、バリデーションなし const { name, age, email } = req.body; const user = userService.createUser(name, age, email); res.json(user);});
// ✅ 良い例: 境界防御の実装import { z } from 'zod';
// スキーマで境界を定義const CreateUserSchema = z.object({ name: z.string().min(1).max(100), age: z.number().int().min(0).max(150), email: z.string().email(),});
app.post('/users', async (req, res) => { try { // バリデーション: Zodで型・形式・範囲を検査 const validatedData = CreateUserSchema.parse(req.body); const user = await userService.createUser(validatedData); res.json(user); } catch (error) { if (error instanceof z.ZodError) { res.status(400).json({ error: error.errors }); } else { res.status(500).json({ error: 'Internal server error' }); } }});なぜ重要か:
- 型安全性: TypeScriptとZodで実行時にも型チェック
- バリデーション: 形式・範囲を検査
- セキュリティ: SQLインジェクション、XSSなどの攻撃を防止
副作用の局所化
Section titled “副作用の局所化”DB更新・通知・外部呼出などの副作用をロジックの末尾に集約し、それ以前を状態を持たない純粋処理として保つ。
// ❌ 悪い例: 副作用が散在async function createOrder(orderData: OrderData): Promise<Order> { // 副作用1: DB更新 const order = await prisma.order.create({ data: orderData });
// ビジネスロジック(副作用が混在) if (order.amount > 10000) { // 副作用2: 外部API呼び出し await notificationService.sendEmail(order.userId); }
// 副作用3: 別のDB更新 await auditLogRepository.create({ data: { event: 'ORDER_CREATED', orderId: order.id }, });
return order;}
// ✅ 良い例: 副作用の局所化async function createOrder(orderData: OrderData): Promise<Order> { // 1. 純粋処理: ビジネスロジック(副作用なし) const order = validateAndCreateOrder(orderData);
// 2. 副作用の集約: すべての副作用を末尾に await persistOrder(order); await notifyIfNeeded(order); await auditOrderCreation(order);
return order;}
// 純粋関数: 副作用なしfunction validateAndCreateOrder(orderData: OrderData): Order { // バリデーションとオブジェクト作成のみ if (orderData.amount <= 0) { throw new Error('Invalid amount'); } return { ...orderData, id: generateId() };}
// 副作用: DB更新async function persistOrder(order: Order): Promise<void> { await prisma.order.create({ data: order });}
// 副作用: 通知async function notifyIfNeeded(order: Order): Promise<void> { if (order.amount > 10000) { await notificationService.sendEmail(order.userId); }}
// 副作用: 監査ログasync function auditOrderCreation(order: Order): Promise<void> { await auditLogRepository.create({ data: { event: 'ORDER_CREATED', orderId: order.id }, });}なぜ重要か:
- テスト容易性: 純粋関数は単体テストが容易
- 可読性: 副作用が明確に分離される
- デバッグ容易性: 副作用の発生箇所が明確
ビジネスロジックが特定ライブラリやORMの仕様に依存しないよう、インターフェース層で抽象化する。
// ❌ 悪い例: ORMに直接依存class OrderService { async findOrder(id: number): Promise<Order> { // Prismaの仕様に依存 return await prisma.order.findUnique({ where: { id }, }); }}
// ✅ 良い例: インターフェースで抽象化// ドメイン層のインターフェースinterface OrderRepository { findById(id: number): Promise<Order | null>; save(order: Order): Promise<Order>;}
// インフラ層の実装class PrismaOrderRepository implements OrderRepository { async findById(id: number): Promise<Order | null> { const result = await prisma.order.findUnique({ where: { id } }); return result ? this.toDomain(result) : null; }
async save(order: Order): Promise<Order> { const result = await prisma.order.create({ data: this.toPersistence(order) }); return this.toDomain(result); }
private toDomain(prismaOrder: any): Order { // Prismaのモデルをドメインモデルに変換 return { ...prismaOrder }; }
private toPersistence(order: Order): any { // ドメインモデルをPrismaのモデルに変換 return { ...order }; }}
// サービス層: ドメイン層のインターフェースに依存class OrderService { constructor(private orderRepository: OrderRepository) {}
async findOrder(id: number): Promise<Order> { const order = await this.orderRepository.findById(id); if (!order) { throw new Error('Order not found'); } return order; }}なぜ重要か:
- 交換容易性: ORMを変更してもビジネスロジックは変更不要
- テスト容易性: モックで簡単にテスト可能
- 保守性: フレームワークの変更に強い
安全に壊れるための設計原則のポイント:
- 境界防御: 外部データは常に汚染されていると仮定し、型・形式・範囲を検査
- 副作用の局所化: 副作用をロジックの末尾に集約し、純粋処理と分離
- 依存の隔離: ビジネスロジックが特定ライブラリに依存しないよう抽象化
これらの原則により、「異常時に安全に壊れる」堅牢なシステムを構築できます。