Skip to content

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

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

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

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

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

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

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