コマンド・クエリ分離原則(CQS) とは?

CQS(Command Query Separation)は、バートランド・メイヤーが提唱した設計原則で、メソッドを以下の2種類に分類します:
  • クエリ: オブジェクトの状態を取得するが、状態を変更しない
  • コマンド: オブジェクトの状態を変更するが、値を返さない
この原則により、メソッドの副作用が明確になり、コードの理解と保守が容易になります。

コマンド・クエリ責任分離(CQRS) とは?

CQRS(Command Query Responsibility Segregation:コマンド・クエリ責任分離)は、CQSの原則をシステムアーキテクチャレベルに拡張した設計パターンです。 単にメソッドを分離するだけでなく、読み取り(クエリ)と書き込み(コマンド)で異なるモデルやデータストアを使用することで、それぞれを最適化できます。

CQS から CQRS への進化

CQS(Command Query Separation)の基本

CQS はメソッドレベルでの分離原則で、各メソッドは「状態を変更する(コマンド)」か「値を返す(クエリ)」のどちらか一方のみを行います。
# CQSの例:1つのクラス内にコマンドとクエリが共存
class Product
  attr_reader :name, :price, :stock

  def initialize(name, price, stock)
    @name = name
    @price = price
    @stock = stock
  end

  # クエリ:価格を取得する(状態を変更しない)
  def current_price
    @price
  end

  # クエリ:在庫を確認する(状態を変更しない)
  def in_stock?
    @stock > 0
  end

  # コマンド:在庫を減らす(値を返さない)
  def reduce_stock(quantity)
    @stock -= quantity
    nil
  end
end

CQS の限界と課題

CQS は優れた原則ですが、システムが複雑になると以下の課題が生じます:
  1. パフォーマンスの問題
# 同じモデルで読み取りと書き込みを行うため、最適化が困難
class Order
  # 複雑な集計クエリ(読み取り最適化が必要)
  def monthly_sales_report
    # 大量のデータを処理...
  end

  # トランザクション処理(書き込み最適化が必要)
  def process_payment
    # 複雑なビジネスロジック...
  end
end
  1. スケーラビリティの制限
  • 読み取りと書き込みの負荷が異なる場合の対応が困難
  • 単一のモデルで両方の要求を満たすのは非効率
  1. ドメインモデルの複雑化
  • 表示用の要求とビジネスロジックが混在
  • モデルが肥大化しやすい

CQRS:CQS の課題を解決する進化形

CQRS は、これらの課題を解決するためにアーキテクチャレベルで読み取りと書き込みを分離します:
# 書き込み側:ビジネスロジックに特化
class ProductCommandService
  def reduce_stock(product_id, quantity)
    product = Product.find(product_id)
    product.reduce_stock(quantity)
    product.save

    # イベントを発行して読み取り側を更新
    EventBus.publish(StockReducedEvent.new(product_id, quantity))
  end
end

# 読み取り側:表示に最適化
class ProductQueryService
  def products_in_stock
    # 非正規化されたデータから高速に取得
    ProductView.where("stock > 0").select(:id, :name, :price)
  end

  def low_stock_products
    # 集計に特化したビュー
    ProductStockView.where("stock < 10")
  end
end
このように、CQRS は CQS の原則を発展させ、より大規模で複雑なシステムに対応できるパターンです。

なぜ コマンド・クエリ責任分離(CQRS) が重要なのか?

従来の設計では、状態を変更するメソッドが同時にデータを返す設計が多く、コードが複雑化しやすい問題がありました。CQRS を用いることで以下のメリットが得られます。
  • コマンドとクエリの責務が明確化され、保守性が向上する
  • データの変更処理をシンプルに保つことができる
  • クエリ側とコマンド側の拡張・最適化を別々に行える

解説

修正前のコード

class UserAccount
  attr_reader :balance

  def initialize(balance = 0)
    @balance = balance
  end

  # 入金処理と残高確認が混ざっている
  def deposit(amount)
    @balance += amount
    @balance # 状態変更とクエリが混ざっている
  end
end

問題点

  • 状態変更(deposit)とクエリ(balance の取得)が混ざっている
  • メソッドの責務が曖昧でテストや保守が難しくなっている

修正後のコード

class UserAccount
  attr_reader :balance

  def initialize(balance = 0)
    @balance = balance
  end

  # コマンド(状態変更のみ)
  def deposit(amount)
    @balance += amount
  end

  # クエリ(状態取得のみ)
  def current_balance
    @balance
  end
end

解決された問題

  • コマンドとクエリが明確に分離され、責務が明確化された
  • テストが容易になり、変更に対して柔軟に対応できるようになった

より実践的な CQRS の例

ここまでの例は主に CQS レベルの分離でしたが、真の CQRS パターンでは読み取りと書き込みで異なるモデルを使用します。

アーキテクチャレベルでの CQRS 実装例

# 書き込み側(Command側)
# これはアプリケーションサービスです(ドメインサービスではありません)
# アプリケーション層でコマンドを処理し、ドメインモデルを調整します
class UserCommandService
  def register_user(command)
    # ドメインモデルを使用した複雑なビジネスロジック
    user = User.new(
      email: command.email,
      name: command.name
    )

    # ビジネスルールの検証
    raise "Invalid email" unless user.valid_email?

    # 永続化
    user_repository.save(user)

    # イベントの発行
    event_bus.publish(
      UserRegisteredEvent.new(
        user_id: user.id,
        email: user.email,
        registered_at: Time.now
      )
    )
  end
end

# 読み取り側(Query側)
class UserQueryService
  def find_active_users
    # 読み取り専用の最適化されたモデル
    UserQuery
      .where(active: true)
      .select(:id, :email, :name, :last_login_at)
      .order(last_login_at: :desc)
  end

  def user_statistics
    # 集計に特化したビュー
    UserStatisticsView.first
  end
end

# 読み取り専用モデル(非正規化されたデータ)
class UserQuery < ActiveRecord::Base
  self.table_name = 'user_queries'

  # 書き込み側のイベントを受けて更新される
  def self.handle_user_registered(event)
    create!(
      user_id: event.user_id,
      email: event.email,
      registered_at: event.registered_at,
      active: true
    )
  end
end
この例では:
  • 書き込み側は複雑なビジネスロジックとドメインモデルを扱う
  • 読み取り側は表示に最適化された非正規化データを扱う
  • イベントを通じて両者の整合性を保つ

サービスの種類について

アプリケーションサービス(UserCommandService)
  • ユースケースを実装する
  • トランザクション境界を管理
  • ドメインモデルを調整
  • 外部システムとの連携を制御
ドメインサービス
  • ドメインロジックのうち、特定のエンティティに属さないもの
  • 複数のエンティティにまたがる処理
  • ビジネスルールの実装
# ドメインサービスの例
class UserRegistrationPolicy
  def can_register?(email)
    # 複雑なビジネスルール
    !blacklisted?(email) &&
    !duplicate?(email) &&
    valid_domain?(email)
  end
end

コマンドの戻り値についての考察

純粋な CQRS では、コマンドは値を返さないことが理想ですが、実際のアプリケーションでは何らかのフィードバックが必要な場合があります。

実践的なアプローチ

# アプローチ1: 成功/失敗のみを返す
class OrderService
  def place_order(command)
    # 処理を実行
    order = create_order(command)

    # 成功/失敗のみを返す
    return { success: true, order_id: order.id }
  rescue => e
    return { success: false, error: e.message }
  end
end

# アプローチ2: イベントベースの非同期処理
class OrderService
  def place_order(command)
    # コマンドIDを生成
    command_id = SecureRandom.uuid

    # 非同期でコマンドを処理
    OrderCommandHandler.perform_async(command_id, command)

    # コマンドIDのみを返す
    { command_id: command_id }
  end
end

# クライアント側でイベントを購読して結果を取得

CQRS の適用における注意点とデメリット

いつ CQRS を使うべきか

適している場合:
  • 読み取りと書き込みの要求が大きく異なる
  • 高いスケーラビリティが必要
  • 複雑なドメインロジックがある
  • イベントソーシングと組み合わせて使用する
適さない場合:
  • シンプルな CRUD アプリケーション
  • 小規模なプロジェクト
  • チームに CQRS の経験がない

デメリットと課題

  1. 複雑性の増加
    • コードベースが大きくなる
    • 理解とメンテナンスが困難になる可能性
  2. 結果整合性
    • 読み取りモデルの更新に遅延が発生する可能性
    • 一時的にデータの不整合が見える場合がある
  3. 開発コスト
    • 初期実装に時間がかかる
    • テストが複雑になる

関連パターンとの組み合わせ

CQRS は以下のパターンと組み合わせることで、より強力になります:
  • Event Sourcing: すべての状態変更をイベントとして保存
  • Domain-Driven Design (DDD): 複雑なビジネスロジックの整理
  • Saga Pattern: 分散トランザクションの管理
  • Read/Write Database 分離: パフォーマンスの最適化

まとめ

CQRS を用いることで、システム内における状態変更と状態取得が明確に分離され、保守性・柔軟性が向上します。 ただし、すべてのアプリケーションに適しているわけではなく、複雑性とのトレードオフを慎重に検討する必要があります。 小規模なプロジェクトでは CQS レベルの分離から始め、必要に応じて本格的な CQRS へと発展させることをお勧めします。

練習問題 (1)

修正前のコード

class ShoppingCart
  attr_reader :items

  def initialize
    @items = []
  end

  def add_item(item)
    @items << item
    total_price
  end

  def total_price
    @items.sum(&:price)
  end
end

修正後のコード

class ShoppingCart
  attr_reader :items

  def initialize
    @items = []
  end

  # コマンド(状態変更のみ)
  def add_item(item)
    @items << item
  end

  # クエリ(状態取得のみ)
  def total_price
    @items.sum(&:price)
  end
end

解説

元のコードでは、アイテム追加時に合計価格を返すという状態変更とクエリが混ざった設計でした。 CQRS を適用し、add_item メソッドは純粋なコマンド(状態変更のみ)に変更し、total_price メソッドを独立したクエリとして設計しました。これにより責務が明確になり、コードのテストと保守が容易になりました。

練習問題 (2)

修正前のコード

class User
  def update_email(new_email)
    self.email = new_email
    save && email
  end
end

修正後のコード

class User
  # コマンド(状態変更)
  def update_email(new_email)
    self.email = new_email
    save
  end

  # クエリ(状態取得)
  def current_email
    email
  end
end

解説

元のコードでは、email 変更の成功時に変更後の email を返却するという、状態変更と取得クエリを混ぜた設計でした。 CQRS を適用し、update_email を純粋なコマンドとして扱い、メールアドレス取得は current_email メソッドに分離しました。これにより、コードの意図が明確になりました。

練習問題 (3)

修正前のコード

class Order
  def cancel
    self.status = 'cancelled'
    save && status
  end
end

修正後のコード

class Order
  # コマンド
  def cancel
    self.status = 'cancelled'
    save
  end

  # クエリ
  def current_status
    status
  end
end

解説

元のコードでは、注文キャンセル処理とステータス取得が同じメソッドに混在していました。 CQRS を導入してキャンセル処理を純粋なコマンドとして分離し、ステータス取得を独立したクエリメソッドとして提供することで、責務を明確化できました。

練習問題 (4)

修正前のコード

class Inventory
  def remove_stock(item, quantity)
    item.stock -= quantity
    item.save && item.stock
  end
end

修正後のコード

class Inventory
  # コマンド
  def remove_stock(item, quantity)
    item.stock -= quantity
    item.save
  end

  # クエリ
  def current_stock(item)
    item.stock
  end
end

解説

元のコードでは在庫の削除処理と在庫数取得が混在していました。 CQRS を適用して、remove_stock という状態変更操作を純粋なコマンドにし、在庫数の取得をクエリとして別途提供しました。これにより、コードの役割が明確になりました。

練習問題 (5)

修正前のコード

class BlogPost
  def publish
    self.published_at = Time.now
    save && published_at
  end
end

修正後のコード

class BlogPost
  # コマンド
  def publish
    self.published_at = Time.now
    save
  end

  # クエリ
  def published_date
    published_at
  end
end

解説

元のコードでは、投稿の公開処理と公開日時取得というコマンドとクエリが混ざっていました。 CQRS に従い、publish メソッドを純粋なコマンドとして定義し、公開日時取得を別途 published_date メソッドで提供することで、処理の責務が明確になり、保守性が向上しました。