こんにちは、鈴木です。
以前のエントリで「includeされた時にクラスメソッドとインスタンスメソッドを同時に追加する頻出パターン」をご紹介しました。
今回は、それに関する定形処理を肩代わりしてくれる ActiveSupport::Concern をご紹介します。
includeされた時にクラスメソッドとインスタンスメソッドを同時に追加するパターン
Before
以前のエントリ(includeされた時にクラスメソッドとインスタンスメソッドを同時に追加する頻出パターン)でご紹介していますが、おさらいしましょう。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
module M def self.included(base) base.extend(ClassMethods) end module ClassMethods def foo puts 'foo' end end def bar puts 'bar' end end |
このモジュール M を include すると、クラスメソッドとして foo が、インスタンスメソッドとして bar が使用できるようになります。
1 2 3 4 5 6 7 8 9 |
class C include M end # foo はクラスメソッド C.foo # bar はインスタンスメソッド. C.new.bar |
ここまでが、おさらいです。
After
上記のようなパターンは ActiveSupport::Concern を用いると、もう少し簡単に書くことができます。以下のコードを見てください。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
module M extend ActiveSupport::Concern module ClassMethods def foo puts 'foo' end end def bar puts 'bar' end end |
ActiveSupport::Concern で extend することと引き換えに「def self.included(base) ... end」の部分が消え去りました。
include 元のクラスメソッドを呼び出すパターン
Before
今度は include 元のクラスメソッドを呼び出すパターンを見てみましょう。
1 2 3 4 5 6 7 8 9 10 11 |
module LogicalDeleteScopes def self.included(base) base.class_eval do scope :without_deleted, lambda{ where(deleted_at: nil) } end end end class Article < ActiveRecord::Base include LogicalDeleteScopes end |
この LogicalDeleteScopes モジュールは ActiveRecord::Base を継承したクラスに include されることを前提としており、include すると「削除日時(deleted_at)が NULL」という条件を追加するスコープ without_deleted が定義されます。
4 行目の「scope :without_deleted, ...」の部分でスコープを定義していますね。include 元のクラスメソッド(scope)を呼び出す必要があるので、「base.class_eval」で囲っています。
使い方は次のようになります。
1 |
Article.without_deleted.all |
After
これを ActiveSupport::Concern を使って書き換えると、次のようになります。
1 2 3 4 5 6 7 8 9 10 |
module LogicalDeleteScopes extend ActiveSupport::Concern included do scope :without_deleted, lambda{ where(deleted_at: nil) } end end class Article < ActiveRecord::Base include LogicalDeleteScopes end |
「def self.included(base) ... end」としていた部分が「included do ... end」に置き換わりました。
class_eval もしなくなっていますが、これは included に渡したブロックが LogicalDeleteScopes の include 元のコンテキスト(つまり Article クラスのコンテキスト)で実行されるからです。
多段階に include するパターン
Before
多段階に include するパターン(モジュールが、さらに別のモジュールに依存するパターン)も見ておきましょう。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
module LogicalDeleteScopes def self.included(base) base.class_eval do scope :without_deleted, lambda{ where(deleted_at: nil) } end end end module PublishStatusScopes def self.included(base) base.class_eval do include LogicalDeleteScopes scope :draft, lambda{ without_deleted.where(status: 'draft') } scope :published, lambda{ without_deleted.where(status: 'published') } end end end class Article < ActiveRecord::Base include PublishStatusScopes end |
Article クラスが PublishStatusScopes に依存し、PublishStatusScopes は LogicalDeleteScopes に依存しています。
サンプルが複雑になってきたので、整理しながら説明しますね。
まず前提条件として、
- Article は何らかの記事を表すクラスであり、公開状態(status)と削除日時(deleted_at)を持つ。
- 公開状態(status)には「下書き状態('draft')」と「公開状態('published')」がある。
- 削除日時(deleted_at)には論理削除された日時が入る。
とします。
その前提で、上記のコードが行なっていることを整理すると、
- LogicalDeleteScopes モジュールは include 元に without_deleted という名前のスコープを追加する。
- PublishStatusScopes モジュールは LogicalDeleteScopes モジュールを include しており、include 元に draft 及び published という名前のスコープを追加する。
- draft 及び published スコープは、LogicalDeleteScopes が定義する without_deleted スコープを利用している。
- Article クラスは PublishStatusScopes モジュールを include している。
ということをしています。
注目していただきたいのは、PublishStatusScopes モジュールで定義される draft と published というスコープです。
1 2 |
scope :draft, lambda{ without_deleted.where(status: 'draft') } scope :published, lambda{ without_deleted.where(status: 'published') } |
LogicalDeleteScopes モジュールによって追加される without_deleted スコープを使用しています。
After
これを ActiveSupport::Concern を使って書き直します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
module LogicalDeleteScopes extend ActiveSupport::Concern included do scope :without_deleted, lambda{ where(deleted_at: nil) } end end module PublishStatusScopes extend ActiveSupport::Concern include LogicalDeleteScopes included do scope :published, lambda{ without_deleted.where(status: 'published') } scope :draft, lambda{ without_deleted.where(status: 'draft') } end end class Article < ActiveRecord::Base include PublishStatusScopes end |
PublishStatusScopes モジュールの実装に注目していただきたいのですが、LogicalDeleteScopes は PublishStatusScopes モジュールに include しています。
普通に考えると PublishStatusScopes に LogicalDeleteScopes を include しても正しく動作しないように思えます(LogicalDeleteScopes は include 元である PublishStatusScopes のコンテキストで without_deleted スコープを定義しようとしますが、PublishStatusScopes 自体は ActiveRecord::Base を継承しているわけではないので「scope というメソッドは存在しない」というエラーになるのでは、と思えます)。
しかしこれで問題無く動作します。というのも、ActiveSupport::Concern がこのようなコードが動作するように依存関係を上手く扱ってくれるからです。
まとめ
include 元にクラスメソッドを追加するパターンや include 元のクラスメソッドを呼び出すパターン、多段階に include するパターンを見てきました。複雑になってくると考慮しなければならないことが増えてきて大変ですが、ActiveSupport::Concern は大きな助けになってくれますね。