🛤️

Railsアーキテクチャパターン: In-App Gems (アプリ内gem) パターン

3 min read 2

なんとなくパターン・ランゲージ(パターンカタログ)のスタイルが懐かしくなったので、あのスタイルを思い出しながら書いてみます。

目的

特定のアプリと完全に同期していますが、機能的には独立したライブラリをアプリ内のgemとして提供するものです。

動機

  • アプリで使いたい機能として、ある程度の独立した機能を実装したい
    • 単独のクラスやモジュールとして実装できるものではなく、複数のクラス等が関連して動く程度の粒度を想定する(ので、個別のファイル単位よりはもう少し大きい粒度になる)
  • 独立した機能とアプリが渾然一体となり、気がつくと境界を侵食したり侵食されたりしてしまうことは避けたい

適用可能性

  • いくつかのclassやmoduleがアプリケーションに依存せず、独立した機能になっている場合
  • 特定のRailsアプリからしか使われる予定がない(共有する必要・予定がない)場合
  • 一定期間しか利用せず、どこかのタイミングで除去する可能性がある場合

なお、機能の大きさによっては、通常のRubyライブラリの場合もあれば、Rails Engineの場合もあります。どちらでも同じように利用できます。

実装

基本的には「アプリにgemを置く場所(ディレクトリ)を作り、そこに普通に作ったgemを並べて置く」というものです。

FooBar gemという名前のgemを追加する場合、以下のようにします。

  • アプリ内に「gemの置き場所」を決めて作る

具体的にどこに置くかは「gemの置き場所」を参考にしてください。

  • 一般的なgemとしてFooBar gemを作成する

gemの置き場所をgems/ディレクトリにする場合、以下のようなコマンドを実行して、gemの雛形を生成します。

$ cd gems
$ bundle gem foo_bar

あとは普通にgemを作っていきます。その際、foo_bar/.gitディレクトリは削除して、アプリのgitとして管理するようにします。

  • アプリのGemfileから、そのgemを読み込む

Gemfileに以下のような記述を追加します。

gem "foo_bar", path: "【gemのディレクトリ】"

gems/ 以下に置いた場合は以下のようになります。

gem "foo_bar", path: "gems/foo_bar"

これで、普通のgemと同様に使えるようになります。

gemの置き場所

gemの置き方にはいくつか候補があります。

  • gems/ 以下

アプリのrootディレクトリにgemsというディレクトリを作り、そこに置くものです。
gemsに何かしらのパスを通したりする必要はありません。

  • vendor/gems/ 以下

Railsの基本的なディレクトリ階層を変えたくない、という場合は、vendorディレクトリ内に置くのが分かりやすそうです。

一方で、vendor/assets 等と並んで存在するのはあまり分かりやすくないと感じられるかもしれません。

  • アプリroot直下

場合によっては、特にディレクトリを用意せずにroot直下に置くこともできます。prefixをつければそれなりに区別しやすいです。

この方式のメリット

  • 依存関係が整理・明確化できる

Railsの柔軟性を素朴に使うと、容易に相互依存しがちになりますが、このパターンを使えば少なくともgemからアプリ本体の依存については抑制しやすくなります。

  • アプリケーションからは「普通のgem」として扱える

Railsも含めて現代的なRubyアプリケーションであれば当然のようにgemを扱うことが多く、そのための手法については十分枯れているかと思います。この方式では、通常のgemで使われる技術はそのまま利用できるため、新しい知識はほぼ必要ありません。

  • バージョン管理のコストが最小で済む

いわゆるmonorepo状態になるため、確実にアプリケーションとの同期がとれた状態になります。gemとアプリのバージョン管理について悩む必要がなくなります。

  • 独立したテストを書きやすい

機能に特化したテストはmodel specでもなければcontroller specでもない……という場合でも、このgemのテストとして通常通りに書けるため、書き手にも読み手にも容易に使いこなせます。

一方で、この機能を利用したRailsアプリ本体のテストは、通常のrequest specやsystem spec等で書くことができます。

この方式のデメリット

そのアプリケーション専用のgemであって、1〜2ファイルで済むような機能であれば、わざわざ独立したgemにする必要性は薄いかもしれません。

また、他のアプリケーションからもこのgemを使いたい、ということであれば、別のリポジトリを作成し、Gemfileからそのリポジトリを参照した方が管理しやすいかと思います。

使用例

Decidim Barcelonaでは、アプリroot直下にDecidim::DatavizDecidim::StatsDecidim::ValidAuthという3つのRails Engineをgemとして置いています。

備考

「Railsアーキテクチャパターン」と言ってみましたが、特にRailsアプリに限らず、gemを使ったRubyアプリケーションであれば普通に使えるパターンです。

この記事に贈られたバッジ