ActiveRecordのモジュール分割を支援するライブラリ active_modularity を作ってみた

Pocket

先日 activerecord-blockwhere の記事を書きましたが、
実は同じ日に active_modularity というGemライブラリも公開しました。

ライブラリの紹介

active_modularityは一つのRailsアプリで管理機能と公開機能など、複数の機能で同じモデル(テーブル)を操作したい場合に利用するライブラリです。

このライブラリを利用する場合、アプリケーションを以下の構造にすることが前提となります。

  • 公開機能、管理機能などのコントローラはPublic,Adminなどのモジュール配下とする
  • トップレベルに前テーブルの継承元となるモデル(基底モデル)を作成する
  • 同じようにモデルもモジュール配下とし、トップレベルの基底モデルを継承する

active_modularityを利用しなくても、上記構成のRailsアプリを構築することは可能ですが、何点か問題が発生します。
この問題点を解消してくれるのがactive_modularityです。

コントローラ、モデルをモジュール分割する

実際にコントローラ、モデルのモジュール分割を行ってみましょう。
例として、簡単なブログサイトの管理機能と公開機能を作る場合を考えます。

コントローラの分割

まずはコントローラをモジュール単位で分割します。
Public::ApplicationControllerのように、モジュール以下で基底となるコントローラを作って共通のフィルタやメソッドを定義します。

注意点として、継承する基底コントローラは、「ApplicationController」ではなく「Public::ApplicationController」のようにモジュール名を省略せず記述する必要があります。
モジュール名を指定しない場合、読み込みの順序次第でトップレベルのApplicationControllerを継承してしまう可能性があるためです。

ルーティングの記述

namespaceとしてpublic,adminを指定し、その中でルーティングを設定します。
public側のurlは/を起点にするため、pathオプションでnilを指定しています。

モデルの分割

モデルはmodelsディレクトリに基底モデルを作り、モジュール配下に継承したモデルを作るようにします。
基底モデルには共通で利用するバリデーションやメソッドなどを定義します。

ビューを作る

各アクションのビューについては特に意識する必要はありません。
レイアウトファイルは、各モジュール内のApplicationControllerに対して指定すると良いでしょう。

公開機能

管理機能

モジュール分割による利点

モデルをモジュール配下に作ることにより、以下のような利点が生まれます。

機能単位で必要な機能を切り替えることができる

各モジュールごとにモデルクラスがわかれるため、コールバックやバリデーション、attr_accessibleなど、
共通で定義したいもの、機能ごとに切り替えたいものを素直に実装することができます。
インスタンス変数にモードを持たせてifで切り替える、などの強引なコードが不要になります。

form_forでurlを指定する必要がなくなる

form_forを利用してフォームを作った場合、formタグのaction属性に設定されるURLはモデルのクラス名を元に構築されます。
モデルをモジュール配下に定義し、コントローラと1対1で対応させることにより、このオプションの指定の必要がなくなります。

以下の3つのform_forのaction属性は全て同じになります。

active_decoratorと相性が良い!

モデルに対してデコレータを定義できる active_decorator というライブラリがあります。
このライブラリを使うと、モデルと1対1で対応するデコーレータ(モジュール)を定義することで、ビュー側のロジックを切り分けることができます。
モデルを機能単位でモジュール配下に分割することにより、管理機能と公開機能で別々のデコーレータが利用できるようになります。

active_decoratorは前回ちょっと触れたactiverecord-refinementsと同じ作者amatsudaさんのライブラリですね。
kaminariをはじめ、いつも使いやすいライブラリをありがとうございます。

モジュール分割による問題点

developmentモードでの問題

Railsをdevelopmentモードで起動している場合、このままでは上手く動かない場合があります。

Public::EntriesController#index アクションでは Entry.all で全エントリを取得しています。
コントローラはPublicモジュールの中に定義されているので、EntryはPublic::Entryクラスとして解決されることを期待しています。

developmentモード(config.cache_class=falseの場合)モデルクラスはconst_missingにより遅延読み込みされますが、Admin::Entryクラスが先に読み込まれていると継承元のEntryクラスが定義されるため、Publicモジュール内でのEntryがトップレベルのEntryとして解釈されてしまうのです。

この問題はコントローラ内でAdmin::Entryのようにモジュール名を省略せずに書けば解消されます。

あるいはinitializerに以下のファイルを追加することでも対応できます。
※このファイルは後々active_modularityの初期化に利用するので、ファイル名はactive_modularity.rbとしています。

起動時に全てのモデルを読み込んでしまうことで、この問題を回避します。
しかし、起動中にモデルクラスを追加した場合は問題が発生する事があるので、その場合はRailsを再起動する必要があります。

関連(Association)先クラスの問題

先ほどのブログサイトにコメント機能を追加することにしました。
has_manyやbelongs_toの関連は機能間で異なることが無いため、基底モデルに記述するのが正しいでしょう。

しかし、関連先のモデルをbuildやcreateしようとした場合、トップレベルにあるモデルクラスのインスタンスが生成されてしまいます。

単一テーブル継承(Single Table Inheritance)クラスの問題

次はブログを編集するためのユーザを作ることにしました。
ユーザにはレビューワと著者という種類があり、単一テーブル継承を利用して実装することにします。

以下のモデルクラスを定義することにしました。

しかし、Admin::Authorクラスをcreateすると、typeカラムには「Admin::Author」のようにモジュール付きのクラス名が登録されてしまいます。
また、Public::Userクラスでのfindは全てのtypeを対象にしたいのですが、type属性がPublic::Userのレコードだけを検索してしまいます。

active_modularityの効果

active_modularityを利用することで、「関連(Association)先クラスの問題」「単一テーブル継承(Single Table Inheritance)クラスの問題」が解消されます。

active_modularityの導入

ではactive_modularityを導入してみます。

インストールはbundlerで行います

次に先ほど作ったinitializerのファイルを以下のように変更します。

設定はこれで完了です。

Association先クラスがモジュール配下になる

もう一度関連モデルのbuildを試してみます。

期待通りの結果になりました。

単一テーブル継承のクラスが変わる

では、次に単一テーブル継承の例を試してみましょう。

少し複雑ですが、単純に表現すると以下の挙動になります。

  • 検索時や登録などtype属性のモジュール名が無視される
  • インスタンス化時はモジュールが考慮される

まとめ

active_modularityはモデルクラスの分割をほんのちょっとだけ手助けしてくれるライブラリです。
モデルクラスを分割するパターンによって、実装を機能ごとに分離することができるので、ぜひ一度お試しください。

Pocket

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です