目次へ

デザインパターンから見た
 Active Record(2)

2012/05/23 シナジーマーケティング(株) 寺岡 佑起

4. 振る舞いを持つモデルクラス

Active Recordパターンにおいて、クラスやインスタンスはただテーブルや行を表すのではなく、それぞれのクラスに応じた振る舞いを持つことができます。
Active Recordライブラリでは、入力値検証やコールバックなど、各モデルに応じたロジックを追加できます。
もちろん、独自にメソッドを定義することも可能です。モデル間の関連の定義もドメインロジックの一種と言えるでしょう。
これらの機能については、次回以降ご紹介していきたいと思います。

Active Recordライブラリを使うと、クラス定義を行うだけでデータベースの読み書きを行えてしまうため、データベースアクセス以外のロジックをアクションメソッド内に書いてしまうこともできます。しかし、モデルに存在すべきドメインロジックをもコントローラに書いてしまうことになるため、Active Recordパターンの正しい使い方とは言えません。
Skinny Controller, Fat Model(太ったモデルと痩せたコントローラ)1という設計思想があります。
これは、コントローラには出来るだけロジックを書かず、モデルに振る舞いを持たせ、ロジックを集中させようという思想です。
なぜ、コントローラにドメインロジックを書かず、モデルクラスに振る舞いを持たせる必要があるのでしょうか?

5. コールバックを利用したモデルクラスの例

以下のような2つのテーブルを例にして考えてみましょう。

■(会社)companies

id
name
employees_count

■(従業員)employees

id
company_id
name

会社モデルにはcompany_idが自身のidと一致する従業員の数を持ちます。
以下の例では、新規従業員を追加した際、従業員数をインクリメントする処理をコントローラに記述しています。

class Company < ActiveRecord::Base
  has_many :employees
end

class Employee < ActiveRecord::Base
  belongs_to :company
end

class EmployeesController < ApplicationController
  # ...省略(indexやnewなどのアクションメソッド)
  
  def create
    Employee.transaction do
      @employee = Employee.create! params[:employee]
      @employee.company.increment(:employees_count) # 従業員数をインクリメント
    end
    redirect_to employees_path
  rescue ActiveRecord::RecordInvalid
    render :action => :new and return
  end
end

この例は正しく動作しますが、ある問題を抱えています。
別のコントローラで従業員の追加処理を行いたくなった場合や、別のモデルの処理の内部で従業員を追加する必要が出てきた場合、従業員追加のコードを記述するそれぞれの場所で、所属する会社のemployees_countをインクリメントするロジックを記述する必要が出てきてしまいます。
これを一つでも忘れた場合、employees_countは正しい数を表さなくなってしまいます。
このような、「従業員モデルが増えた際に所属する会社のemployees_countをインクリメントする」というルールは、モデル間の整合性を保つ上で必ず守られなければなりません。
つまり、この処理を行う責任はコントローラではなくモデル側にあるということになります。

この振る舞いを従業員モデルに定義してみましょう。

class Company < ActiveRecord::Base
  has_many :employees
end

class Employee < ActiveRecord::Base
  belongs_to :company
  
  after_create do
    company.increment(:employees_count)
  end
  
  after_destroy do
    company.decrement(:employees_count)
  end
end

class EmployeesController < ApplicationController
  # ...省略(indexやnewなどのアクションメソッド)
  
  def create
    @employee = Employee.create! params[:post]
    redirect_to employees_path
  rescue ActiveRecord::RecordInvalid
    render :action => :new and return
  end
end

after_create、after_destroyに与えられたブロックは、Active Recordライブラリのcallback機能によりデータベースレコードの作成、削除直後に実行されます。
これにより、従業員レコードが追加されれば会社レコードのemployees_countが自動的にインクリメントされ、削除されればデクリメントされるようになります。
この定義を行うことで、コントローラは従業員レコードを追加する際、会社レコードの更新について意識する必要がなくなりした。
別の箇所で従業員レコードを追加・削除しても、自動的に会社モデルのemployees_countは更新されていきます。
このようにすることで、モデルが守らなければいけない制約はモデルクラスに集約され、利用者(今回はコントローラ)側はActive Record共通のインターフェース(create!メソッド)を利用するだけで良くなり、再利用性の向上、保守性の向上が見込めます。
Active Recordでクラス設計を行う際は、出来るだけモデルクラスに処理を集め、コントローラの操作によってデータに不整合が起きないように設計すべきでしょう。

6. まとめ

Active Recordパターンを利用するとドメインロジックをモデルに集中させることができ、Skinny Controller, Fat Modelを実践することができます。2
Active Recordライブラリには、Active Recordパターン以外の機能も沢山実装されており、
上記で利用したcallbackの他にも、以下のような便利な機能がたくさんあります。

  • Validations(入力値検証)
  • Callbacks(コールバック)
  • Associations(関連)
  • Single Table Inheritance(単一テーブル継承)
  • Polymorphic association(ポリモーフィック関連)
  • Scoping(NamedScope) 等

実は、先ほどのコールバックの例も、Active Recordライブラリではcounter_cacheという機能として実装されており、以下のコードを書くだけで実現できてしまいます。

class Employee < ActiveRecord::Base
  belongs_to :company, :counter_cache => true
end

これらの機能を上手く利用すれば、少ないコード量で非常に強力なモデルクラスを作成することが可能になります。
次回以降、Active Recordライブラリの機能についてご紹介していきたいと思います。

1 { buckblogs :here }: Skinny Controller, Fat Model(英語サイト)

2 モデルクラスに処理を集めた結果、巨大になりすぎるようであれば、さらにモジュールなどに分割することを考える必要があるでしょう。

↑このページの先頭へ

こちらもチェック!

PR
  • XMLDB.jp
  • シナジーマーケティング研究開発グループブログ