安全に壊れるための設計原則
安全に壊れるための設計原則
Section titled “安全に壊れるための設計原則”「正常に動く」よりも「異常時に安全に壊れる」ことを優先する設計原則を詳しく解説します。
境界防御 (Boundary Defense)
Section titled “境界防御 (Boundary Defense)”外部(API・DB・ユーザ入力)からのデータは常に汚染されていると仮定し、型・形式・範囲を検査してからロジックに渡す。
// ❌ 悪い例: 無防備な入力受付@RestControllerpublic class UserController { @PostMapping("/users") public User createUser(@RequestBody Map<String, Object> data) { // 問題: 型チェックなし、バリデーションなし String name = (String) data.get("name"); Integer age = (Integer) data.get("age"); return userService.createUser(name, age); }}
// ✅ 良い例: 境界防御の実装@RestControllerpublic class UserController { @PostMapping("/users") public ResponseEntity<User> createUser(@Valid @RequestBody CreateUserRequest request) { // バリデーション: Bean Validationで型・形式・範囲を検査 User user = userService.createUser(request); return ResponseEntity.ok(user); }}
// DTOで境界を定義public class CreateUserRequest { @NotBlank @Size(min = 1, max = 100) private String name;
@NotNull @Min(0) @Max(150) private Integer age;
@Email private String email;}なぜ重要か:
- 型安全性: コンパイル時に型エラーを検出
- バリデーション: 実行時に形式・範囲を検査
- セキュリティ: SQLインジェクション、XSSなどの攻撃を防止
副作用の局所化
Section titled “副作用の局所化”DB更新・通知・外部呼出などの副作用をロジックの末尾に集約し、それ以前を状態を持たない純粋処理として保つ。
// ❌ 悪い例: 副作用が散在@Servicepublic class OrderService { public Order createOrder(OrderData orderData) { // 副作用1: DB更新 Order order = orderRepository.save(new Order(orderData));
// ビジネスロジック(副作用が混在) if (order.getAmount() > 10000) { // 副作用2: 外部API呼び出し notificationService.sendEmail(order.getUserId()); }
// 副作用3: 別のDB更新 auditLogRepository.save(new AuditLog("ORDER_CREATED", order.getId()));
return order; }}
// ✅ 良い例: 副作用の局所化@Servicepublic class OrderService { public Order createOrder(OrderData orderData) { // 1. 純粋処理: ビジネスロジック(副作用なし) Order order = validateAndCreateOrder(orderData);
// 2. 副作用の集約: すべての副作用を末尾に persistOrder(order); notifyIfNeeded(order); auditOrderCreation(order);
return order; }
// 純粋関数: 副作用なし private Order validateAndCreateOrder(OrderData orderData) { // バリデーションとオブジェクト作成のみ if (orderData.getAmount() <= 0) { throw new IllegalArgumentException("Invalid amount"); } return new Order(orderData); }
// 副作用: DB更新 private void persistOrder(Order order) { orderRepository.save(order); }
// 副作用: 通知 private void notifyIfNeeded(Order order) { if (order.getAmount() > 10000) { notificationService.sendEmail(order.getUserId()); } }
// 副作用: 監査ログ private void auditOrderCreation(Order order) { auditLogRepository.save(new AuditLog("ORDER_CREATED", order.getId())); }}なぜ重要か:
- テスト容易性: 純粋関数は単体テストが容易
- 可読性: 副作用が明確に分離される
- デバッグ容易性: 副作用の発生箇所が明確
ビジネスロジックが特定ライブラリやORMの仕様に依存しないよう、インターフェース層で抽象化する。
// ❌ 悪い例: ORMに直接依存@Servicepublic class OrderService { @Autowired private OrderRepository orderRepository; // JPAに直接依存
public Order findOrder(Long id) { // JPAの仕様に依存 return orderRepository.findById(id) .orElseThrow(() -> new OrderNotFoundException(id)); }}
// ✅ 良い例: インターフェースで抽象化// ドメイン層のインターフェースpublic interface OrderRepository { Order findById(Long id); void save(Order order);}
// インフラ層の実装@Repositorypublic class JpaOrderRepository implements OrderRepository { private final SpringDataOrderRepository springDataRepo;
@Override public Order findById(Long id) { return springDataRepo.findById(id) .map(this::toDomain) .orElseThrow(() -> new OrderNotFoundException(id)); }}
// サービス層: ドメイン層のインターフェースに依存@Servicepublic class OrderService { private final OrderRepository orderRepository; // インターフェースに依存
public Order findOrder(Long id) { return orderRepository.findById(id); }}なぜ重要か:
- 交換容易性: ORMを変更してもビジネスロジックは変更不要
- テスト容易性: モックで簡単にテスト可能
- 保守性: フレームワークの変更に強い
安全に壊れるための設計原則のポイント:
- 境界防御: 外部データは常に汚染されていると仮定し、型・形式・範囲を検査
- 副作用の局所化: 副作用をロジックの末尾に集約し、純粋処理と分離
- 依存の隔離: ビジネスロジックが特定ライブラリに依存しないよう抽象化
これらの原則により、「異常時に安全に壊れる」堅牢なシステムを構築できます。