Status | Authors | Coach | DRIs | Owning Stage | Created |
---|---|---|---|---|---|
proposed |
@fabiopitino
| 2023-05-22 |
六角形のレール モノリス
要約
TL;DR:Railsのモノリスを泥の状態の大きなボールから、六角形アーキテクチャ(またはポートとアダプタアーキテクチャ)を使用するモジュール式のモノリスに変更します。ドメイン駆動設計のプラクティスを使用して、まとまりのある機能ドメインを個別のディレクトリ構造に抽出します。インフラストラクチャのコード(ロギング、データベースツール、インスツルメンテーションなど)をgemsに抽出し、lib/
ディレクトリの必要性を本質的に取り除きます。機能ドメインのどの部分(例えばアプリケーションサービス)がインテグレーションに公開され(ポート)、どの部分が非公開のカプセル化された詳細部分であるかを定義します。アーキテクチャの外部レイヤーのアダプタとして、Web、Sidekiq、REST、GraphQL、アクションケーブルを定義します。モノリスのモジュール間のプライバシーと依存性を強制するためにPackwerkを使用します。
詳細
適用領域
アプリケーションコア(機能ドメイン)は、GitLab製品に固有のビジネスロジック、ポリシー、データを記述するすべてのコードで構成されます。トップレベルのバウンデッドコンテキストに分かれています。バウンデッドコンテキストはRubyモジュールの形で表されます。これは名前空間の命名に関する既存のガイドラインに従っていますが、より構造化されています。
モジュールは
- 内部ロジック、ステート、データの多くをカプセル化するのに十分な深さ。
- 可能な限り小さく、他の制限されたコンテキストで安全に使用でき、十分に文書化された公開インターフェースを持つこと。
- まとまりがあり、記述する機能のSSoT(単一真実源)を表すこと。
機能カテゴリは、モジュールが深くなるのに十分な大きさの製品領域を表すので、小さなトップレベルモジュールが増殖することはありません。また、コードベースがユビキタス言語に従うのにも役立ちます。チームは、複数の機能カテゴリを担当することができ、複数の境界コンテキストのビジョンを所有することができます。フィーチャー・カテゴリーは、時にオーナーが変わることがありますが、バウンデッド・コンテキストを新しいオーナーにマッピングするこの変更は、非常に安価です。フィーチャーカテゴリーを使うことは、GitLabチームのメンバーや、より広いコミュニティのメンバーである新しいコントリビューターがコードベースをナビゲートするのにも役立ちます。
複数のフィーチャーカテゴリーが強く関連している場合、それらを一つのバウンデッドコンテキストの下にグループ化することができます。機能カテゴリが親機能カテゴリのコンテキストでのみ関連する場合は、親機能カテゴリの境界コンテキストに含めることができます。たとえば継続的インテグレーション機能カテゴリのコンテキストに存在するビルド アーティファクトは、単一のバウンデッド コンテキストの下にマージすることができます。
アプリケーション・ドメインは、アプリケーション・アダプターのような外部レイヤーの知識を持たず、プラットフォーム・コードにのみ依存します。これにより、ドメインコードはビジネスロジックのSSoTとなり、WebUIまたはREST APIからのリクエストに関係なく再利用可能でテスト可能になります。
外部レイヤと内部レイヤの間の依存関係 (ドメインコードがアダプタのインターフェイスに依存する) が必要な場合、これは制御の反転テクニック、特に依存性の注入を使用して解決できます。
アプリケーションアダプタ
アダプタはコンポーネントと外部世界の間の接着剤です。 外界と アプリケーションコンポーネント 内部の要求を表すポートとの間のやり取りを調整_します。例えば、_データは GUI やコマンドラインインタフェースを通してユーザーから 提供さ_れたり、自動化されたデータソースから提供されたり、テストスクリプトから_提供されたりします。 -ウィキペディア
アプリケーションアダプタは
- Web UI (Rails コントローラ、ビュー、JS、Vue クライアント)
- REST APIエンドポイント
- GraphQLエンドポイント
エンドポイントはユーザーとの対話を担当します。各アダプターはリクエストを解釈し、パラメーターを解析し、アプリケーションドメインから適切な抽象化を呼び出し、結果をユーザーに返します。
プレゼンテーションロジックや、場合によっては認証は、 アダプタ層固有のものです。
アプリケーションアダプタ層は、Railsフレームワーク、アダプタを動かすgem、設定、ユーティリティなど、実行するプラットフォームコードに依存します。
プラットフォームコード
プラットフォーム・コードでは、アプリケーション・ドメインやアプリケーション・アダプタが動作するために必要なクラスやモジュールを考えます。
現在のRailsのlib/
ディレクトリには、他の場所に置くことができる複数のカテゴリのコードが含まれており、そのほとんどがプラットフォームコードです:
- REST APIエンドポイントは、アプリケーション・アダプタの一部である可能性があります。
- ドメインコード(
Gitlab::Ci
のような大きなドメインコードも、Gitlab::JiraImport
のような小さなドメインコードも)は、アプリケーションドメインの内部に移動するべきです。 - 残りは、モノリス内部の
gems/
ディレクトリの下に、個別の単一目的のgemとして抽出することができます。これには、ロギング、エラーレポート、メトリクスなどのユーティリティ、レートリミッタ、Gitlab::ApplicationRateLimiter
,Gitlab::Redis
,Gitlab::Database
のようなインフラストラクチャコード、Banzai
のような汎用サブドメインが含まれます。
ApplicationRecord
やApplicationWorker
のようなRailsフレームワークを拡張するベースクラスや、BaseService
のようなGitLabベースクラスは、gemの拡張として実装することができます。
つまり、Railsフレームワークのコードを除けば、残りのプラットフォームコードはgems/
に存在します。
最終的には、gems/
内部のコードはすべて別のリポジトリに抽出されるか、オープンソース化される可能性があります。gems/
内にプラットフォーム コードを配置することで、その目的がアプリケーション コードに役立つことであることが明確になります。
境界の強制
Rubyには、モジュール内の定数のプライバシーという概念がありません。他のプログラミング言語とは異なり、Rubyではすべての定数が公開されているため、きちんと文書化されたgemを抜き出しても、他の開発者がコードと実装の詳細を結びつけることを防ぐことはできません。
Rubyではすべての定数が公開されているため、コードベースが六角形アーキテクチャで完璧に構成されていても、コードベースの最大の部分であるアプリケーションドメインがモジュール化されていない大きな泥の塊であることがあります。
境界を強制することは、構造を長期的に維持するためにも不可欠です。大きなモジュール化の努力の後、境界を侵すことで徐々に大きな泥の玉に戻ってしまうようなことは避けたいのです。
私たちは、モジュール境界を強制するための概念実証でPackwerkを使うアイデアを探りました。
Packwerkは、コードベースに徐々にパッケージを導入し、プライバシーと明示的な依存関係を強制することを可能にする静的アナライザです。Packwerkは、あるRubyコードが他のパッケージの非公開実装の詳細を使用しているかどうか、または依存関係として明示的に宣言されていないパッケージを使用しているかどうかを検出することができます。
静的アナライザであるため、コードの実行には影響を与えず、Packwerkの導入は安全で、徐々に行うことができます。
Gustoのような会社は、Packwerkを中心としたRailsのモジュラーモノリスを使用するように移行したい組織のために、開発ツールやエンジニアリングツールのリストを開発し、メンテナーしています。
EEとJHの拡張機能
GitLabのコードベースをモジュール化するユニークなチャレンジの一つは、EEエクステンション(GitLabが管理)とJHエクステンション(JiHuが管理)の存在です。
関連するドメインのコード(例えばCi::
)を同じバウンデッドコンテキストとPackwerkパッケージの下に移動させることで、その中のee/
拡張も移動させる必要があります。
トップレベルのバウンデッドコンテキストをPackwerkパッケージにも一致させるためには、特定のドメインに関連するすべてのコードを、例えばEEエクステンションも含めて、同じパッケージディレクトリの下に配置する必要があることを意味します。
以下はディレクトリ構造の一例です:
domains
├── ci
│ ├── package.yml # package definition.
│ ├── packwerk.yml # tool configurations for this package.
│ ├── package_todo.yml # existing violations.
│ ├── core # Core features available in Community Edition and always autoloaded.
│ │ ├── app
│ │ │ ├── models/...
│ │ │ ├── services/...
│ │ │ └── lib/... # domain-specific `lib` moved inside `app` together with other classes.
│ │ └── spec
│ │ └── models/...
│ ├── ee # EE extensions specific to the bounded context, conditionally autoloaded.
│ │ ├── models/...
│ │ └── spec
│ │ └── models/...
│ └── public # Public constants are placed here so they can be referenced by other packages.
│ ├── core
│ │ ├── app
│ │ │ └── models/...
│ │ └── spec
│ │ └── models/...
│ └── ee
│ ├── app
│ │ └── models/...
│ └── spec
│ └── models/...
├── merge_requests/
├── repositories/
└── ...
課題
- このような変更には、モジュラーアーキテクチャの利点を理解し、レガシーな慣習に逆戻りしないよう、開発者の考え方を変える必要があります。
- アプリケーションアーキテクチャの変更は困難な作業です。時間、リソース、コミットメントが必要ですが、最も重要なことはエンジニアの賛同が必要だということです。
- このため、アーキテクチャの進化計画を進展させ、さまざまなエンジニアリングチャネルで議論を促進し、採用の課題を解決するエンジニアの中期的なチームやワーキンググループを持つ必要があるかもしれません。
- 私たちは、サイロではなく、標準とガイドラインを確実に構築する必要があります。
- 新しいコードをどこに置くべきか、明確なガイドラインを持つ必要があります。
lib/
のようなガラクタの引き出しのようなフォルダを作り直さないようにしなければなりません。
チャンス
モジュラー・モノリス・アーキテクチャへの移行は、私たちが将来探求できる多くの機会を可能にします:
- モノリスの特定のモジュールを明示的に所有することで、ドメイン・エキスパートの概念に沿うことができます。
- 静的解析ツール(PackwerkやRubocopなど)を使用することで、開発やCIにおける設計違反を検出し、ベストプラクティスが守られるようにすることができます。
- モジュール間の依存関係を明示的に定義することで、変更の影響を受ける部分のみをテストすることでCIを高速化できます。
- このようなモジュラーアーキテクチャは、必要に応じてモジュールをさらに別のサービスに分解するのに役立ちます。