Goのレイヤード設計方式
(jerf.org)- Go言語は パッケージ間の循環参照を厳格に禁止 しているため、これは自然に 階層的設計(layered design) を促す
- この記事は、Goプロジェクトが 必然的に持つことになる階層構造 を説明し、その上に別個のアーキテクチャを強制しなくても十分に有効だと主張する
- 循環依存が発生した場合、それを解決するための 具体的で実用的なリファクタリング戦略 を段階的に提示する
- 各パッケージは独立して意味のある機能単位を持つように設計され、テスト、保守、マイクロサービス分離 にも有利
- 結果としてこの方式は、実際のコード設計でよく起こる 「バナナが欲しかったのにジャングルごと持ってくる」問題 を防ぐ
Goにおけるレイヤード設計アプローチ
基本原則
- Goは パッケージ間の循環参照を禁止 する
- すべてのGoプログラムのimport関係は 有向非巡回グラフ(DAG) を構成しなければならない
- この構造は選択ではなく 言語レベルで強制される設計規則 である
パッケージレイヤリングの自動形成
- 外部パッケージを除くプロジェクト内部のパッケージは、参照の深さに応じて自動的にレイヤリング可能
- 下図のように最下層には metrics、logging、共通データ構造などの中核ユーティリティパッケージ が位置する
- その後、上位パッケージが徐々に 機能を組み合わせながら上に積み上がる構造 を形成する
この設計方式の特性
- レイヤは 階層的抽象化ではなく参照の方向性に基づく
- 1つのパッケージは 複数の下位レベルのパッケージを参照可能
- MVC、ヘキサゴナルアーキテクチャなど既存の設計方式も、この構造の上に 「適用」 できる
→ ただし、Goの構造的制約を必ず考慮する必要がある
循環参照の解決戦略
循環参照が発生した場合は、以下の順でリファクタリングを試みる:
1. 機能の移動
- 最も推奨される方法
- 循環を引き起こす機能を 正確に分析 し、論理的に 適切な場所へ移す
- 頻繁に使う方法ではないが、概念的な明確さを最も向上させる
2. 共通機能を別パッケージに分離
- 両側で共通に使う型や関数(
Usernameなど)を 第3のパッケージへ移動 する - パッケージが小さく見えても思い切って分離する
→ 時間がたつにつれて、そのパッケージが大きく育つ可能性が高い
3. 上位の合成パッケージを作成
- 循環している2つのパッケージを 組み合わせる第3のパッケージ を作成する
- 例:
Category、BlogPostの双方向依存を上位パッケージへ分離
→ 下位パッケージは dumb struct のまま維持し、実際の機能は上位パッケージで組み合わせる
4. インターフェースの導入
- 構造体や関数が必要とするメソッドだけを持つ インターフェースで依存性を置き換える
- 不要な依存性を除去し、テストのしやすさ を確保する
- ただし、使いすぎるとかえって設計が複雑になることがある
5. コピー(Copy)
- 依存対象が非常に小さい場合は、単純にコピーして使う
- DRY違反に見えるかもしれないが、実際には設計の明確化に役立つ ことも多い
6. 1つのパッケージに統合
- 上記の方法がすべて不可能なら 2つのパッケージを統合 する
- 大きすぎるパッケージにならないなら許容可能
→ ただし、無条件の統合は避け、慎重に判断する
この設計方式の実用的な利点
- 各パッケージは それ自体で意味のある機能単位 を持ち、独立してテスト可能
- パッケージ内の参照が制限されるため、コード全体を理解しなくても個別パッケージを理解できる
- 意図しない全体依存の連結(=ジャングル問題)を避け、必要なものだけを使うコードを書くよう促す
- マイクロサービス分離時にも容易に抽出可能
→ ほとんどの依存関係が明確に定義されている
結論
- Goのパッケージ設計上の制約は煩わしい制約ではなく 良い設計を促す装置 である
- 特別なアーキテクチャがなくても パッケージ間の参照構造だけで堅牢な設計を実現可能
- 循環参照に対する 精緻な分析とリファクタリング戦略 は、Goだけでなく他の言語にも有効
4件のコメント
最初はとりあえず書いて動かしていると楽しいけれど
テストを書き始めると
あのときどうしてあんなふうにしたんだろうと考えるようになります。
「バナナが欲しかったのにジャングルを持ってこられた」という言い回し、すごく面白いですね。
Springで開発するとき、いちばん大変なことの一つは依存関係の循環だった気がしますね..
無限にお互いを初期化しながら、メモリリークで落ちてしまうあのもどかしさといったら...
Hacker Newsのコメント
循環依存を許可しないのは、大規模なプログラムを構築するうえで優れた設計上の選択である
素晴らしいブログ記事である
「3つ目のパッケージに移す」という助言に関連するおまけのテクニック
Yourdonの構造化手法に関する本を読んでいるようだ
パッケージは互いに循環参照できない
ランダマイザの具体概念を思い起こさせる
Golangの面白い特徴は、パッケージレベルでは循環依存を持てないが、go.modでは持てることだ
Jerfがパッケージをどう考え、循環依存をどう扱うかについての見事な説明である