横断的ライブラリを gem に抽出する
このページは、横断的な プラットフォーム(ライブラリ)コードを lib/ と ee/lib から gems/ 配下の Ruby gem へ抽出するための単一の情報源です。この決定は ADR-004: 横断的ライブラリを gem に抽出するに記録されています。
ここで扱うのは、汎用的で横断的なライブラリの抽出だけです。機能ドメインの抽出、トランスポート / レイヤーの gem、デプロイモードは 扱いません。それらはモジュラーモノリス設計の別の場所で説明される個別の関心事です。
プラットフォームコードとは何か?
プラットフォームコードは、六角形モノリスの第 3 の層であり、アプリケーションドメインとアプリケーションアダプターに並ぶものです。これは、ドメインとアダプターが実行するために依存するが、それ自体にはビジネスロジックを持たないクラスやモジュールの集合です。
これらは本物の 横断的関心事 です。ロギング、エラーレポート、メトリクス、レートリミッター、パーサー、Banzai のような汎用ユーティリティです。特定のドメインについて何も知らないからこそ、多くのドメインに役立ちます。
真のプラットフォームコードの定義的な性質は、ActiveRecord モデルに依存しない ことです。コードが Project、User、service、または任意の AR モデルに手を伸ばすなら、それはほぼ確実に ドメインコード です。gems/ ではなく、そのドメインに属します。「汎用」を疑うを参照してください。
問題
Rails の lib/gitlab/ ディレクトリは「何でも入れ」になっています。約 3,100 ファイル(約 94,000 行)があり、非常に異なる 3 種類のコードが混在しています:
- 汎用 / universal — ActiveRecord 依存のないユーティリティ、パーサー、データ構造。真のプラットフォームコードです。
- GitLab 固有だが分離済み — GitLab の概念を参照するがアプリケーションモデルは参照しないライブラリ。これも抽出可能です。
- 密結合 — AR モデル、service、Rails ランタイムに依存するコード。これは
lib/のふりをしたドメインコードです。
カテゴリ 1 には間違いなく抽出候補が含まれます。カテゴリ 2 は、ドメイン固有ライブラリをどのようにグループ化するかの決定次第です。カテゴリ 3 は違います。バウンデッドコンテキストへ移すべきです。
この混在は依存関係を隠し、所有権を曖昧にし、汎用に見えるコードが静かにアプリケーションへ結合することを許します。
目標
- 強制された分離 — gem は依存関係を明示的に宣言します。隠れた依存関係は、静かな結合ではなく見えるエラーになります。
- より明確な所有権 — 各 gem は明示的な
feature_category:を持ち、チーム境界へマッピングされます。 - パターンとツールを確立する — 後続の分解作業で再利用できる、繰り返し可能な抽出レシピと CI の配線。
- より速い CI — 実際の、ただし限定的なメリットです。下記の注意点を参照してください。
「汎用」を疑う
汎用に見えるものすべてを抽出すべきではありません。「ライブラリ」が実はドメインロジックであることを示す最も強い匂いは、それを動作させるために AR データを注入する 必要があることです。
誘惑されるパターンは、gem から AR 依存を外しておくために、モノリスがコールバックを通じて専用のデータ構造を与えることです:
# The gem defines the shape of data it needs
Gitlab::Diff::FileInfo = Struct.new(:path, :content, :size, keyword_init: true)
# The monolith wires a lambda that maps an AR object to that shape
Gitlab::Diff.config.find_file = ->(id) {
blob = Repository.find_blob(id)
Gitlab::Diff::FileInfo.new(path: blob.path, content: blob.data, size: blob.size)
}
これは慎重に扱ってください。「汎用ライブラリ」に AR 由来データを与えるために依存性注入やコールバックに手を伸ばしているなら、そのコードは通常、まったく汎用ではないというサインです。それはバウンデッドコンテキストへ移すべきドメインロジックです。コールバックは慎重かつ意図的に使い、すべての抽出のハッピーパスにしないでください。
名前空間
抽出は、コードに適切な名前空間を与える機会です。
- プラットフォーム / 非ドメインコード は
Gitlab::名前空間を保持できます。ただし、Gitlab::は衝突回避以上の価値をほとんど加えないという継続中の議論があります。コードが何をするものかは何も伝えません。 lib/gitlab/から取り出されたドメインコード はGitlab::を 保持すべきではありません。config/bounded_contexts.ymlに従って、明示的なバウンデッドコンテキストの名前空間へ移すべきです。
抽出パターン
リポジトリ内のすべての gem は gems/ に存在し、Gemfile から path: で参照されます。
ファイルを移動する —
lib/gitlab/<name>/(および任意のee/lib/gitlab/<name>/)をgems/gitlab-<name>/へ移します。プラットフォームコードは名前空間を保持し、ドメインコードは名前空間を付け直します(名前空間を参照)。AR 非依存を保つ — ライブラリ gem は AR モデルを参照してはいけません。アプリケーションデータが必要な場合、そのコードが本当にライブラリなのかを再考するシグナルです。
Gemfile へ配線する
gem 'gitlab-<name>', path: 'gems/gitlab-<name>', require: false, feature_category: :<category>EE 拡張 — EE コードは gem ディレクトリ内の
ee/フォルダーに置きます。gem のエントリーポイントは、EE コンテキストで条件付きにee/ツリーをロードします。gems/gitlab-<name>/ lib/ gitlab/<name>/... ee/ gitlab/<name>/... # EE overrides spec/gem ごとの CI — 各 gem は
gems/<name>/.gitlab-ci.ymlと.gitlab/ci/gitlab-gems.gitlab-ci.ymlのエントリを持ち、独自の分離されたテストスイートを実行します。
候補
以下の候補は、モノリスからどれだけきれいに分離できるかの順に並べた例示的な階層です。Tier 1 と Tier 2 の AR 非依存部分だけが真のライブラリ候補です。密結合なコードは自身のドメインに属します。
Tier 1 — 低結合、高価値
| ライブラリ | 現在の場所 | メモ |
|---|---|---|
ci/parsers + ci/reports | lib/gitlab/ci/parsers/, lib/gitlab/ci/reports/ | 解析 / レポート用データ構造 |
regex | lib/gitlab/regex/ | 純粋な Ruby |
slug | lib/gitlab/slug/ | 純粋なユーティリティ |
json | lib/gitlab/json/ | Oj/JSON のラッパー |
sanitizers | lib/gitlab/sanitizers/ | HTML / text のサニタイズ |
diff | lib/gitlab/diff/ | Diff の解析と整形 |
template_parser | lib/gitlab/template_parser/ | テンプレート解析ロジック |
word_diff | lib/gitlab/word_diff/ | 単語単位の diff |
Tier 2 — 中程度の結合、明確な継ぎ目
これらには、分離可能な汎用コアと AR 結合のグルーが含まれます。汎用コアだけを抽出し、AR 結合部分はそのドメインに残します。
| ライブラリ | 現在の場所 | 残すもの |
|---|---|---|
pagination | lib/gitlab/pagination/ | Keyset pagination は汎用。AR スコープは残す |
search | lib/gitlab/search/ | クエリ構築は AR スコープから分離可能 |
changelog | lib/gitlab/changelog/ | テンプレート処理は汎用。commit / repository access は残す |
Tier 3 — AR に結合しているが可能性はある
このコードは AR モデル、Rails ランタイム、またはすべてのドメインを横断するものに依存しています。これはライブラリでは ありません。モノリスに残るか、バウンデッドコンテキストへ移ります。
| ライブラリ | 理由 |
|---|---|
auth | Devise、OmniAuth、session、すべての model に触れる |
background_migration | AR を継承し、schema を直接参照する |
event_store | すべてのドメインをまたいで公開 / 購読する |
sidekiq_middleware | Rails ミドルウェアスタックに依存する |
順序付けと循環依存
コードを gem へ抽出することは アトミックなリファクタリング です。1 回のステップで、コードはすべての依存関係を宣言し、完全に分離されていなければなりません。その厳密さこそが、gem を明示的な依存関係の強制に適したものにしています。しかし、大規模または複雑で、段階的にしか分離できないコードにはつらいものです。
そこから 2 つのガイダンスが得られます。
下から始める。 依存関係を持たない最下層のコードから始め、上に向かって進みます。これにより、半分だけ抽出された gem 間で循環依存を作ることを避けられます。
粗くではなく細かく始める。 小さく焦点を絞った gem を 1 つずつ抽出します。きれいに分離できる Tier 1 ライブラリにはすでに良い継ぎ目があるため、それぞれはマージ、テスト、リバートしやすい小さなアトミックリファクタリングです。これにより、抽出パターンと CI 配線をより早く実証できます。
任意: Packwerk 支援の段階的分離
1 回のアトミックな移動では分離できないコードについては、Packwerk(静的解析)が段階的な経路を 支援 できます。ファイルを移動せず、ランタイム挙動を変えずにパッケージ境界を引き、依存関係とプライバシーの違反を表面化できます。十分な分離が達成されると、最終的な gem 抽出ははるかに単純になります。
Packwerk が生成する明示的な依存関係とプライバシーのグラフは、AI エージェント型リファクタリング にも有用です。コード片が何に依存し、何がそれに依存しているかについて、正確で機械可読な地図をエージェントに与えるため、エージェントは暗黙的な結合から推測するのではなく、抽出の影響範囲を推論できます。これにより、Packwerk はエージェント駆動の抽出作業に役立つ支援になります。
Packwerk は段階的な経路のための 任意で補完的な ツールであり、主要なアプローチではありません。そしてハンドブックではこれをプルーフオブコンセプト / 提案として扱い、コミット済みのアプローチとはしていません。主要な経路は、最下層のコードから始める直接的でアトミックな gem 抽出のままです。
CI 高速化に関する注意
コードを gem に分離すると、それが変更されたとき、またはそれが依存するものが変更されたときだけ CI でテストできます。毎回モノリスのスイートの一部として再実行する必要がなくなります。これこそ、安定した コードの分離に価値がある理由です。lib/gitlab/ のライブラリはおおむね安定しており、ほとんど変更されません。そのため、gem 境界の背後に置かれると、そのスイートを実行する必要はほとんどなくなります。安定して変更頻度の低いコードは、この種の CI 節約に最も適した候補であり、最悪の候補ではありません。
メリットは実際にありますが、無制限ではありません。密結合なシステムでは gem 同士の相互接続が残ります。多くのドメインが CI に依存し、CI も多くの gem に依存します。そのため、多くから依存される gem が変更された場合、そのパイプラインは依然としてすべての依存先へ波及します。効果は、変更頻度の低いコードを分離して、その波及をほとんど起こさないようにすることから生まれます。波及そのものをなくすことからではありません。
未解決事項
- gem ごとの明示的な依存関係か、共有プラットフォーム gem か? すべてのドメイン gem またはモジュールが、例えば gemspec 内ですべての依存関係を明示的に宣言すべきでしょうか。それとも、ドメインが実行に必要とする基本的なツール群(ロギング、エラーレポート、オブザーバビリティ、strong-memoize など)を提供する単一の “platform” gem、LabKit または類似のものに依存すべきでしょうか。明示的な依存関係は結合を完全に可視化し、各 gem を最小に保ちます。共有プラットフォーム gem は定型コードを減らし、すべてのドメインに一貫したベースラインを与えますが、粗く広く依存される依存関係というコストがあります。
- EE 拡張はどのように扱うか? 各 gem は EE コードを
ee/フォルダーに持ちますが、EE 専用の 依存関係 をどのように宣言するかは未解決です。gem がee/ツリーでだけ使用する依存関係を必要とする場合、それは gemspec で無条件に宣言するのでしょうか、それとも条件付きでしょうか。そしてそれはGemfileとGemfile.eeの分割を意味するのでしょうか。これは GitLab FOSS と EE をどのようにパッケージ化するかに依存する可能性が高いです。
c955a93f)