RailsエンジンをRuboCopで徹底的に分離する:前編(翻訳)

概要 原著者の許諾を得て翻訳・公開いたします。 英語記事: Isolating Rails Engines with RuboCop – Flexport Engineering 原文公開日: 2019/11/10 著者: Max Heinritz サイト: Flexport Engineering — 流通系のシステムを手掛けている開発会社です 日本語タイトルは内容に即したものにしました。画像はすべて元記事からの引用です。 RailsエンジンをRuboCopで徹底的に分離する:前編(翻訳) FlexportのメインとなるバックエンドサービスはRuby on Railsモノリスです。弊社を立ち上げた頃はRailsのおかげでビジネスを素早く進めることができました。しかし、成長著しいスタートアップによくあることではありますが、チームが育つに連れて複雑さを管理するのが困難になってきました。 当初はRailsの利便性のおかげで生産性が向上しましたが、今やそのせいで何が起こっているかを理解するのも困難なありさまです。大量の双方向モデル関連付けやら、ActiveRecordで何でも読み書きできてしまっているとか、グローバルなapp/ディレクトリ構造やら暗黙の振る舞いやら、もうきりがありません。 複雑極まる状態になったアプリを解きほぐすために、私たちはRailsエンジンを使い始めました。Railsエンジンとは、あるRailsアプリの中に内蔵されるモジュールで、独自のディレクトリ構造や名前空間を備えています。しかしながら、Railsエンジンが提供するモジュラリティのほとんどは外面的なものにとどまります。エンジニアがエンジンの内部に直接手を突っ込んでごそごそやることを防いではくれません。 Flexportでは、Railsエンジンのデフォルトの振る舞いを拡張するために、エンジン内部に強めの制約をかけて分離する手法をいくつか編み出しました。本記事では弊社のアプローチとして、Railsエンジンに関連したRuboCopのcopを3つご紹介します。3つのcopはオープンソースとして公開していますので、コミュニティで広く共有いただけます。 オープンソースのステータス: rubocop-flexportgemおよびリポジトリを一般公開しています。 Railsエンジンの分離について 「Railsエンジンの分離」とは、エンジンの外部にあるコードからエンジン内部を自由に読み書きできないようにすること、そしてその逆に、エンジン内部からエンジン外部を自由に読み書きできないようにすることを指します。 まずは簡単なコード例から。デフォルトのapp/の他にoceanとtrucking(運輸)という2つのエンジンがあるとします。ディレクトリ構造は以下のとおりです。 メインのRailsアプリと2つのエンジンのディレクトリ構造分離されたRailsサブディレクトリには、それぞれにmodels/やcontrollers/やservices/といったディレクトリがあります。 さて、oceanチームのエンジニアは、出荷されたコンテナが宛先の港に到着する日時を知りたいとします。そこでoceanエンジン内にこんなハンドラを1つ追加しました。 oceanエンジン内の、到着するコンテナのハンドラその後、truckingチームもコンテナがいつ到着するのか知りたくなったとしましょう。truckingチームのエンジニアはoceanのハンドラに少々コードを追加して、truckingエンジン内に手を突っ込んでモデルを更新するようにしました。 oceanエンジンの内部からtruckingエンジンのモデルに直接アクセスしているこのやり方は便利ですが、エンジン間の越境は「関心の分離」に違反しています。oceanエンジンはtruckingエンジン内部のモデルについてもビジネスプロセスについても知識を持ってしまいました。oceanエンジンにはActive Recordモデルへの参照があるので、ocean側のコードがtrucking側のフィールドを勝手に設定してしまう可能性があります。 oceanエンジンはtruckingモデルに直接アクセスしてtruckingの内部に書き込めてしまう「エンジンの強制分離」は、この種のエンジン間の越境をプログラム的に防止することです。 エンジン強制分離のメリット 弊社の主要な目標は、エンジン同士の関心の分離と高い凝縮度、そして疎結合を達成することです。エンジン強制分離を導入することで、開発中にこれらの原則に違反すれば機能を早くリリースするのに便利な場合であっても、開発者をこれらの原則に従わせます。エンジン強制分離によって、戦術上においても以下のようなさまざまなメリットを得られます。 1. 自由な書き込みを防止する Active Recordのモデルに直接アクセスすると、コードベースのどんな場所からでも.saveで自由気ままに書き込みできてしまいます。これでは書き込みパスをチームで一本化するのが難しくなり、コードもわかりにくくなります。 「この配送に用いる運搬車両を変更する」といった単純なビジネスプロセスですら、バリデーションやコールバックや外部コードがあちこちに散らばってしまう可能性がある。 モデル自身にもモデルをまたがる重要なバリデーションを含めなければならなくなる: これによってバリデーションで関連付けが読み込まれるときのパフォーマンスが低下し、さらにN + 1クエリ問題につながることもあります。 メール送信やサードパーティシステムとの同期、イベント発火といった副作用があちこちでトリガされ、デバッグが困難になる可能性がある。 2. 自由な読み取りを防止する Active Recordのモデルに直接アクセスすると、関連付け(association)を無視して他のモデルから自由気ままにデータを読めてしまい、次のような結果になってしまいます。 … Continue reading RailsエンジンをRuboCopで徹底的に分離する:前編(翻訳)