ActiveSupport::Concern でハッピーなモジュールライフを送る

こんにちは、鈴木です。

 

以前のエントリで「includeされた時にクラスメソッドとインスタンスメソッドを同時に追加する頻出パターン」をご紹介しました。

今回は、それに関する定形処理を肩代わりしてくれる ActiveSupport::Concern をご紹介します。

 

includeされた時にクラスメソッドとインスタンスメソッドを同時に追加するパターン

Before

以前のエントリ(includeされた時にクラスメソッドとインスタンスメソッドを同時に追加する頻出パターン)でご紹介していますが、おさらいしましょう。

このモジュール M を include すると、クラスメソッドとして foo が、インスタンスメソッドとして bar が使用できるようになります。

ここまでが、おさらいです。

After

上記のようなパターンは ActiveSupport::Concern を用いると、もう少し簡単に書くことができます。以下のコードを見てください。

ActiveSupport::Concern で extend することと引き換えに「def self.included(base) ... end」の部分が消え去りました。

 

include 元のクラスメソッドを呼び出すパターン

Before

今度は include 元のクラスメソッドを呼び出すパターンを見てみましょう。

この LogicalDeleteScopes モジュールは ActiveRecord::Base を継承したクラスに include されることを前提としており、include すると「削除日時(deleted_at)が NULL」という条件を追加するスコープ without_deleted が定義されます。

4 行目の「scope :without_deleted, ...」の部分でスコープを定義していますね。include 元のクラスメソッド(scope)を呼び出す必要があるので、「base.class_eval」で囲っています。

使い方は次のようになります。

After

これを ActiveSupport::Concern を使って書き換えると、次のようになります。

「def self.included(base) ... end」としていた部分が「included do ... end」に置き換わりました。

class_eval もしなくなっていますが、これは included に渡したブロックが LogicalDeleteScopes の include 元のコンテキスト(つまり Article クラスのコンテキスト)で実行されるからです。

 

多段階に include するパターン

Before

多段階に include するパターン(モジュールが、さらに別のモジュールに依存するパターン)も見ておきましょう。

Article クラスが PublishStatusScopes に依存し、PublishStatusScopes は LogicalDeleteScopes に依存しています。

サンプルが複雑になってきたので、整理しながら説明しますね。

まず前提条件として、

  • Article は何らかの記事を表すクラスであり、公開状態(status)と削除日時(deleted_at)を持つ。
  • 公開状態(status)には「下書き状態('draft')」と「公開状態('published')」がある。
  • 削除日時(deleted_at)には論理削除された日時が入る。

とします。

その前提で、上記のコードが行なっていることを整理すると、

  1. LogicalDeleteScopes モジュールは include 元に without_deleted という名前のスコープを追加する。
  2. PublishStatusScopes モジュールは LogicalDeleteScopes モジュールを include しており、include 元に draft 及び published という名前のスコープを追加する。
  3. draft 及び published スコープは、LogicalDeleteScopes が定義する without_deleted スコープを利用している。
  4. Article クラスは PublishStatusScopes モジュールを include している。

ということをしています。

注目していただきたいのは、PublishStatusScopes モジュールで定義される draft と published というスコープです。

LogicalDeleteScopes モジュールによって追加される without_deleted スコープを使用しています。

After

これを ActiveSupport::Concern を使って書き直します。

PublishStatusScopes モジュールの実装に注目していただきたいのですが、LogicalDeleteScopes は PublishStatusScopes モジュールに include しています

普通に考えると PublishStatusScopes に LogicalDeleteScopes を include しても正しく動作しないように思えます(LogicalDeleteScopes は include 元である PublishStatusScopes のコンテキストで without_deleted スコープを定義しようとしますが、PublishStatusScopes 自体は ActiveRecord::Base を継承しているわけではないので「scope というメソッドは存在しない」というエラーになるのでは、と思えます)。

しかしこれで問題無く動作します。というのも、ActiveSupport::Concern がこのようなコードが動作するように依存関係を上手く扱ってくれるからです。

 

まとめ

include 元にクラスメソッドを追加するパターンや include 元のクラスメソッドを呼び出すパターン、多段階に include するパターンを見てきました。複雑になってくると考慮しなければならないことが増えてきて大変ですが、ActiveSupport::Concern は大きな助けになってくれますね。

 

Comments are closed, but you can leave a trackback: Trackback URL.