Skip to content

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

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

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

# ❌ 悪い例: 無防備な入力受付
class UsersController < ApplicationController
def create
# 問題: 型チェックなし、バリデーションなし
@user = User.create(params[:user])
render json: @user
end
end
# ✅ 良い例: 境界防御の実装
class UsersController < ApplicationController
def create
@user = User.new(user_params)
if @user.save
render json: @user, status: :created
else
render json: { errors: @user.errors }, status: :unprocessable_entity
end
end
private
def user_params
# Strong Parametersで境界を定義
params.require(:user).permit(:name, :age, :email)
end
end
# モデルでバリデーション
class User < ApplicationRecord
validates :name, presence: true, length: { minimum: 1, maximum: 100 }
validates :age, presence: true, numericality: { greater_than_or_equal_to: 0, less_than_or_equal_to: 150 }
validates :email, presence: true, format: { with: URI::MailTo::EMAIL_REGEXP }
end

なぜ重要か:

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

DB更新・通知・外部呼出などの副作用をロジックの末尾に集約し、それ以前を状態を持たない純粋処理として保つ。

# ❌ 悪い例: 副作用が散在
class OrderService
def create_order(order_data)
# 副作用1: DB更新
order = Order.create!(order_data)
# ビジネスロジック(副作用が混在)
if order.amount > 10000
# 副作用2: 外部API呼び出し
NotificationService.new.send_email(order.user_id)
end
# 副作用3: 別のDB更新
AuditLog.create!(event: 'ORDER_CREATED', order_id: order.id)
order
end
end
# ✅ 良い例: 副作用の局所化
class OrderService
def create_order(order_data)
# 1. 純粋処理: ビジネスロジック(副作用なし)
order = validate_and_create_order(order_data)
# 2. 副作用の集約: すべての副作用を末尾に
persist_order(order)
notify_if_needed(order)
audit_order_creation(order)
order
end
private
# 純粋関数: 副作用なし
def validate_and_create_order(order_data)
# バリデーションとオブジェクト作成のみ
raise ArgumentError, 'Invalid amount' if order_data[:amount] <= 0
Order.new(order_data)
end
# 副作用: DB更新
def persist_order(order)
order.save!
end
# 副作用: 通知
def notify_if_needed(order)
NotificationService.new.send_email(order.user_id) if order.amount > 10000
end
# 副作用: 監査ログ
def audit_order_creation(order)
AuditLog.create!(event: 'ORDER_CREATED', order_id: order.id)
end
end

なぜ重要か:

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

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

# ❌ 悪い例: ActiveRecordに直接依存
class OrderService
def find_order(id)
# ActiveRecordの仕様に依存
Order.find(id)
end
end
# ✅ 良い例: インターフェースで抽象化
# ドメイン層のインターフェース
module OrderRepository
def find_by_id(id)
raise NotImplementedError
end
def save(order)
raise NotImplementedError
end
end
# インフラ層の実装
class ActiveRecordOrderRepository
include OrderRepository
def find_by_id(id)
Order.find(id)
rescue ActiveRecord::RecordNotFound
nil
end
def save(order)
order.save!
end
end
# サービス層: ドメイン層のインターフェースに依存
class OrderService
def initialize(repository: ActiveRecordOrderRepository.new)
@repository = repository
end
def find_order(id)
order = @repository.find_by_id(id)
raise OrderNotFoundError, id unless order
order
end
end

なぜ重要か:

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

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

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

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