Railsエンジンは使いすぎに注意(翻訳)
有名なDevise gemを認証に使ったことは誰しもあると思いますが、このgemは「エンジン」でもあります。Railsエンジンはミニチュア版のRailsアプリケーションで、独自のapp/フォルダを持ち、その中にコントローラやモデルなどを含んでいます。唯一の違いは、エンジンは単体では動作できず、動かすにはメインのRailsアプリケーションに注入する必要がある点です。
Railsエンジンが導入されたのは、再利用可能なモジュラーコンポーネントを作成して、アーキテクチャの肥大化に対処するためでした。しかし、薬も過ぎれば毒となります。
Railsエンジンは、十分吟味された設計で実装されれば強力な武器となりますが、そうでないとさまざまな問題を引き起こす可能性があります。
アプリの情報がエンジンに漏洩する
アプリケーションの情報がエンジンに漏れ出すと、エンジンがその情報を元に思わぬ振る舞いをする可能性があります。
たとえば、あるエンジンがメインアプリのauthor_name
を受け取り、これを用いてユーザーを検索してPostモデルに関連付けようと試みているとします。このデータベースは共有されているので、User
が見つからない場合はエンジンがUser
を作成しますが、エンジンがメインアプリの知識を持たないままUser
を作成すると深刻な問題につながる可能性があります。
module Engine
class Post < ActiveRecord::Base
attr_accessor :author_name
belongs_to :author, class_name: "User"
before_validation :set_author
private
def set_author
self.author = User.find_or_create_by(name: author_name)
end
end
end
このEngine
はデータベースを完全に支配していて、User
に対してどんな操作でもできてしまうこともわかります。
それだけではありません。たとえばメインアプリに posts_controller
とposts_path
ルーティングヘルパーがあるとすると、エンジンがposts_path
ヘルパーに越境アクセスしようとしてもエラーにならず、メインアプリとエンジンの境界で漏洩が発生する可能性があります。
コードベースをまたがる共有コードでない限りエンジンを使わないこと
Railsエンジンを複数プロジェクト間で共有する予定がない限り、Railsエンジンを使う意味はありません。エンジンを使っても、アーキテクチャには有意義なカプセル化がほとんど追加されず、単にあるRailsプロジェクトを他のプロジェクトにミックスインする手段にしかなりません。
もし私たちが次世代Deviseをgemとしてリリースする計画があるなら有用でしょうが、そのgemがプロジェクトで重要な位置を占めることになるので苦痛の元になります。エンジンの変更は、必然的にプロジェクトの他の部分の変更よりも難しくなるからです。そういうわけで、機能をエンジンにする前に、モジュールやライブラリの形にできるかどうかを常にあらかじめ検討しておくのがよいでしょう。
エンジンのコードはテストが難しい
エンジンではTDD(テスト駆動開発)がほぼ通用しません。エンジンのテストを実行するには、エンジンを完全なダミーRailsアプリとデータベースの中で動かす必要がありますが、これはプロジェクトの開発チームにとって負担になります。
メインアプリのモデルに依存するエンジンのテストを書くには、RailsガイドのRailsエンジン入門にかかれているように、メインアプリのモデルをエンジン内のtest/dummy/ディレクトリに生成し、このダミーアプリにエンジンをマウントする必要があります。
エンジンはコードの凝集性を引き下げ、アプリのコードと癒着する
ここで言う凝集性とは、アプリケーションが独立して進化できるようにすることです。つまり、あるコンポーネントを変更したときに他のコンポーネントを変更する必要がないようにします。
しかしエンジンの場合はそうではありません。たとえば、ブログエンジンがユーザーをname
属性で検索しているとします。このname
属性がusername
にリネームされると、エンジン側のコードも更新しないとusername
を利用できなくなります。
module Engine
class Post < ActiveRecord::Base
attr_accessor :author_name
belongs_to :author, class_name: "User"
before_validation :set_author
private
def set_author
self.author = User.find_or_create_by(name: author_name)
end
end
end
この問題が発生する原因は、アプリケーションとエンジンの間にBounded Context(境界を持つコンテキスト)が定義されていないからです。
また、エンジンとアプリは同じデータベースを共有します。データストレージから切り離されたインターフェイスを作成しておくことで、独自のストレージを持つ完全に切り離されたサービスをスムーズに構築できるようになります。さらに私たちの場合は、データベースをインターフェイスとして使わずに、データ操作と振る舞いをエンジン内で明示的に定義するようにしています。
不要なコードや依存関係が増えてしまう
Railsエンジンは独自のapp/ディレクトリと.gemspecファイルを持つので、バージョン管理システムにチェックインされる冗長なコードが不必要に増えてしまいます。そのため、不要なコードや依存関係をクリーンアップしようとすると、メインアプリだけではなくエンジンの面倒も見なければならなくなります。
コンフィグや.gemspecが複数になる
Railsエンジンを使うと、メインアプリとエンジンそれぞれでコンフィグを作成しなければならず、微妙かつ深刻な問題が生じる可能性があります。
エンジンの依存関係はGemfileではなく.gemspecで定義します。つまり、使うgemはRubygems.orgなどのgemサーバー上から取得可能である必要があり、GitHubリポジトリから直接読み込めません。gemをローカルで使うことは常に可能ですが、これはこれで面倒です。また、エンジンとメインアプリの依存関係のバージョンがかけ離れないよう注意する必要もあります。
たとえば、メインアプリでDevise 4.0を使っていて、そのエンジンがDevise 3.0を異なるコンフィグで使っていると、コンフィグがエンジンのイニシャライザではなくメインアプリのイニシャライザで読み込まれるため、Deviseのエンジンが期待どおりに動きません。
概要
元サイトの許諾を得て翻訳・公開いたします。