Skip to content

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

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

外部(API・DOM・ユーザ入力)からのデータは常に汚染されていると仮定し、型・形式・範囲を検査してからロジックに渡す。

// ❌ 悪い例: 無防備な入力受付
function createUser(data: any) {
// 問題: 型チェックなし、バリデーションなし
return {
name: data.name,
age: data.age,
email: data.email,
};
}
// ✅ 良い例: 境界防御の実装
interface UserData {
name: string;
age: number;
email: string;
}
function validateUserData(data: unknown): UserData {
if (typeof data !== 'object' || data === null) {
throw new Error('Invalid data type');
}
const obj = data as Record<string, unknown>;
if (typeof obj.name !== 'string' || obj.name.length === 0 || obj.name.length > 100) {
throw new Error('Invalid name');
}
if (typeof obj.age !== 'number' || obj.age < 0 || obj.age > 150) {
throw new Error('Invalid age');
}
if (typeof obj.email !== 'string' || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(obj.email)) {
throw new Error('Invalid email');
}
return {
name: obj.name,
age: obj.age,
email: obj.email,
};
}
function createUser(data: unknown) {
const validated = validateUserData(data);
return validated;
}

なぜ重要か:

  • 型安全性: TypeScriptの型システムで型チェック
  • バリデーション: 形式・範囲を検査
  • セキュリティ: XSS、インジェクションなどの攻撃を防止

DOM更新・API呼び出し・ストレージ操作などの副作用をロジックの末尾に集約し、それ以前を状態を持たない純粋処理として保つ。

// ❌ 悪い例: 副作用が散在
function processOrder(orderData: any) {
// 副作用1: DOM更新
const element = document.getElementById('order-status')!;
element.textContent = 'Processing...';
// ビジネスロジック(副作用が混在)
const total = orderData.items.reduce((sum: number, item: any) => {
return sum + item.price * item.quantity;
}, 0);
if (total > 10000) {
// 副作用2: API呼び出し
fetch('/api/notify', { method: 'POST', body: JSON.stringify({ total }) });
}
// 副作用3: ストレージ操作
localStorage.setItem('lastOrder', JSON.stringify(orderData));
return total;
}
// ✅ 良い例: 副作用の局所化
function calculateTotal(items: OrderItem[]): number {
// 純粋関数: 副作用なし
return items.reduce((sum, item) => {
return sum + item.price * item.quantity;
}, 0);
}
function processOrder(orderData: OrderData) {
// 1. 純粋処理: ビジネスロジック(副作用なし)
const total = calculateTotal(orderData.items);
// 2. 副作用の集約: すべての副作用を末尾に
updateDOM(total);
notifyIfNeeded(total);
saveToStorage(orderData);
return total;
}
function updateDOM(total: number) {
const element = document.getElementById('order-status');
if (element) {
element.textContent = `Total: ${total}`;
}
}
function notifyIfNeeded(total: number) {
if (total > 10000) {
fetch('/api/notify', { method: 'POST', body: JSON.stringify({ total }) });
}
}
function saveToStorage(orderData: OrderData) {
localStorage.setItem('lastOrder', JSON.stringify(orderData));
}

なぜ重要か:

  • テスト容易性: 純粋関数は単体テストが容易
  • 可読性: 副作用が明確に分離される
  • デバッグ容易性: 副作用の発生箇所が明確

ビジネスロジックが特定ライブラリやDOM APIの仕様に依存しないよう、インターフェース層で抽象化する。

// ❌ 悪い例: DOM APIに直接依存
class OrderService {
displayOrder(order: Order) {
// DOM APIの仕様に依存
const element = document.getElementById('order')!;
element.textContent = `Order: ${order.id}`;
}
}
// ✅ 良い例: インターフェースで抽象化
interface View {
displayOrder(order: Order): void;
}
class DOMView implements View {
constructor(private containerId: string) {}
displayOrder(order: Order) {
const element = document.getElementById(this.containerId);
if (element) {
element.textContent = `Order: ${order.id}`;
}
}
}
class ConsoleView implements View {
displayOrder(order: Order) {
console.log(`Order: ${order.id}`);
}
}
// サービス層: インターフェースに依存
class OrderService {
constructor(private view: View) {}
displayOrder(order: Order) {
this.view.displayOrder(order);
}
}
// 使用例
const domView = new DOMView('order');
const orderService = new OrderService(domView);
orderService.displayOrder({ id: 1, total: 1000 });

なぜ重要か:

  • 交換容易性: DOM APIを変更してもビジネスロジックは変更不要
  • テスト容易性: モックで簡単にテスト可能
  • 保守性: フレームワークの変更に強い

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

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

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