Tech Racho エンジニアの「?」を「!」に。
  • Ruby / Rails関連

Railsエンジンは使いすぎに注意(翻訳)

概要

元サイトの許諾を得て翻訳・公開いたします。

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_controllerposts_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のエンジンが期待どおりに動きません。

関連記事

Rails 7: マルチプルDBのreading_request?がカスタマイズ可能になった(翻訳)


CONTACT

TechRachoでは、パートナーシップをご検討いただける方からの
ご連絡をお待ちしております。ぜひお気軽にご意見・ご相談ください。