モジュール境界設計スキル
機能の境界をどこに引き、何を同じ側に置き、何を分けるかを判断するための思考手順。
原則
機能境界は 処理の順序 ではなく 変更理由の独立性 で決まる。
そしてもう一つ。境界を「引く」だけでは不十分で、その境界を「成立させる」必要がある。責務を分けただけでは境界にはならない。境界が意味を持つのは、公開面・状態の所有・依存方向・不変条件の守備範囲が明確になったときである。
適用範囲
このスキルを使う場面
- 機能の境界が曖昧で、どこで切るべきか判断が必要なとき
- モジュールやクラスが肥大化していて分割を検討するとき
- 変更の影響が広範囲に波及している構造を改善するとき
- 抽象化の妥当性に疑いがあるとき
- 既存の責務配置を見直すとき
このスキルの対象外
- 単純な CRUD の実装判断
- 差分レベルのコードレビュー
- API のインターフェース設計(境界は決まっていて表面だけ考える場合)
- 依存方向の違反検出だけが目的の場合
思考手順
Step 1: 機能文の構造化
設計対象の機能を「〈主語〉が〈対象〉を〈操作〉する」の形に分解する。複数の操作が含まれていれば、操作ごとに分けて書き出す。
ユーザーが使った言葉をそのまま使わない。「何を受け取り、何を保全し、何を変換し、何を出力するか」という責務が見える動詞に置き換える。
Step 2: 境界候補の発見
以下の4つの観点から、境界の候補を洗い出す。どれか1つではなく、4つすべてを順に確認する。
2-A: 対象の置換(共通性・可変性の識別)
機能文の中の対象(目的語)を、現実的にありうる別の対象で3つ以上置き換える。
- どの置換でも処理が変わらない部分 → 共通層(抽象化してよい)
- 置換すると処理の内容・ルール・制約が変わる部分 → 専用層(ドメイン固有に残す)
3つ以上なのは、2つでは偶然の一致と本質的な共通性の区別がつかないため。ただし3つ目が現実に存在しない場合は、無理に一般化せず継ぎ目だけを意識する。
2-B: 意味の分裂の検出
同じ言葉(名詞)が、文脈によって異なる意味で使われていないかを確認する。
同じ「ユーザー」でも、認証では「認証主体」、課金では「支払者」、サポートでは「問い合わせ者」を意味するなら、それぞれ別の境界に属する可能性が高い。意味が変わる地点は境界候補になる。
確認方法:
- 機能文に登場する主要な名詞を列挙する
- 各名詞の意味が、機能のどの文脈でも同一かを問う
- 意味がずれる地点があれば、そこに境界候補を置く
- 境界をまたぐ場合に翻訳(変換層)が必要かを判断する
2-C: 不変条件の特定
「一緒に壊れてはいけないルール」を特定する。あるルール群が1つのトランザクションや1つの整合性制約で結びついているなら、それらは同じ境界に留めるべき可能性が高い。
確認方法:
- この機能が守るべきビジネスルール・整合性制約は何かを列挙する
- そのルールが成立するために、どのデータが同時に参照・更新される必要があるかを特定する
- 同じ不変条件に縛られるデータと処理は、同じ境界に置く
不変条件が境界をまたぐ場合、調整コスト(saga、eventual consistency、手動整合)が発生する。そのコストを許容できるかどうかが、境界を分けるかどうかの判断材料になる。
2-D: 所有と変更頻度の確認
誰が、どのくらいの頻度で、その部分を変更するかを確認する。
- 変更の起点が異なるもの(法務起点 vs 開発起点 vs 顧客要望起点)は、別の境界候補
- 変更頻度が極端に異なるもの(年1回変わるルール vs 週次で変わるルール)は、分離した方が安定する
- 所有者(継続的に面倒を見る人・チーム)が異なるものは、別の境界にした方が実務上強い
所有者が曖昧な境界は、設計がどれだけ綺麗でも時間とともに崩壊する。
Step 3: 変化軸の特定と境界の確定
Step 2 で見つけた境界候補を「なぜ変わるのか」で分類し、最終的な境界を確定する。
- 「この部分が変わるとき、他のどこが一緒に変わるか?」→ 一緒に変わるものは同じ境界
- 「この部分が変わるとき、他はそのままでいられるか?」→ いられるなら、そこが境界
変化軸が異なるものは、見た目が似ていても・処理フロー上は隣接していても分ける。変化軸が同じものは、処理フロー上は離れていても束ねる。
Step 4: 差分の吸収方法の選択
境界内の可変部分をどう設計に取り込むかを、差分の性質に応じて選ぶ。
値だけが異なる場合(型・構造は同一): パラメータ化する。
振る舞い・アルゴリズムが異なる場合: インターフェースを抽出し、実装を差し替え可能にする。
処理の流れ自体が異なる場合: 別のモジュール・サービスとして独立させる。
判断の閾値:
- パラメータが3つ以下で条件分岐が増えない → パラメータ化してよい
- パラメータに応じて内部の制御フローが変わる → インターフェース抽出を検討
- 統合後に型チェックや条件分岐が増えた → 抽象を解体して戻す
Step 5: 境界の成立条件の確認
境界を引いたら、その境界が実際に機能するかを確認する。境界が成立するには、以下の4つが明確である必要がある。
公開面(Public Contract): この境界が外部に提供するインターフェースは何か。何を受け取り、何を返すか。公開面が言語化できないなら、境界が不明確である。
状態の所有: この境界が排他的に所有するデータは何か。他の境界が直接読み書きしてよいデータはあるか。共有される可変状態があるなら、境界は名目上のものでしかない。
依存方向: この境界は何に依存し、何がこの境界に依存するか。循環依存があれば、境界の引き方を見直す。
通信方式: 境界間はどう連携するか。直接呼び出し、イベント、非同期メッセージ、変換層のどれが適切かは、境界の独立性の要求水準で決まる。
Step 6: 変更マトリクスによる検証
想定される変更シナリオを5つ程度並べ、各シナリオで影響を受けるモジュールを特定する。影響が1〜2モジュールに閉じていれば妥当。3モジュール以上に波及するなら、境界の引き直しを検討する。
Step 7: 抽象化の範囲の制限
抽象化は「広げること」だけでなく「受け入れる可変性を制限すること」でもある。
- 初期実装では今ある具体例だけで動くものを作る
- インターフェースは切っておくが実装は1つだけにする
- 2つ目・3つ目の具体的な要件が現れた時点で、必要な分だけ一般化する
まだ存在しない要件のために抽象化すると、必要な柔軟性ではなく不要な複雑性が生まれる。間違った抽象を共有するより、重複を許容する方がコストが低い。
重要な区別: 境界設計と内部構造は別の問題
設計相談では、以下が頻繁に混同される。
- 境界設計: どの責務をどのモジュールに配置するか。意味・変化軸・不変条件・所有で決める
- 内部構造: 境界の中をどう整理するか。vertical slice、layer、CQRS などは内部戦略
この2つは別の問題であり、別の判断基準で決める。「feature ごとに切るべきか、domain ごとに切るべきか」という問いが出たら、まず「それは境界の話か、内部構造の話か」を切り分ける。
命名の原則
責務名で命名する。実装技術名で命名しない。 名前が「何の技術を使っているか」を説明しているなら実装名であり、悪い兆候。名前が「何の責任を負っているか」を説明しているなら責務名であり、良い兆候。
1つの名前に複数の責務が含まれていないか検査する。 And / Or / Manager / Handler / Processor のような汎用語は、複数の変化軸が1つに混ざっている可能性を示す。
反パターン
1. フロー順分割
処理フローの順番(Step1, Step2, Step3)でモジュールを切る。変化軸をまたぐ境界になりやすい。
2. 間違った抽象の共有
共通化した結果、対象種別ごとの条件分岐・パラメータ・例外ケースが増殖する。統合前より複雑になったら、抽象を解体して重複に戻す。
3. 全能サービス
XxxManager, XxxHandler, XxxProcessor に責務が集中する。検出したら、そこに含まれる責務を列挙し、変化軸ごとに分離候補を出す。
4. 貧血ドメインモデル
データだけがエンティティにあり、ロジックがすべてサービスにある。境界内の責務配置が崩れているサイン。データとそのデータに対する操作が別の場所に散っている場合、それらを同じ場所に寄せることを検討する。
5. 名詞の類似性による統合
名前が似ているという理由だけで処理を統合する。操作集合・不変条件・エラー境界が一致しない限り、統合は避ける。
6. 過剰な先行抽象
具体例が1つしかないのに汎用フレームワークを作る。「将来必要になるかもしれない」は分割の根拠にならない。
7. 状態の暗黙共有
パッケージやフォルダは分かれているが、可変な状態を複数モジュールが直接読み書きしている。境界は名目上のものでしかなく、実質的に密結合。
8. 肥大化した境界
1つの境界の中に、実際には独立に変化する複数のサブドメインが同居している。境界自体は正しいが、粒度が粗すぎる。分割が必要になったら、最も変更頻度が高く他と独立しているサブドメインから切り出す。
理論的根拠
共通性・可変性分析(SCV Analysis)
Coplien, Hoffman, Weiss ら。対象集合の全要素に成り立つ仮定を共通性、一部にしか成り立たない属性を可変性と定義し、可変性を識別した上で制限する。Step 2-A と Step 7 の根拠。
情報隠蔽(Information Hiding)
Parnas (1972)。モジュール分割の基準は処理フローの順序ではなく、独立に変わりそうな設計決定の隔離。本スキルの原則。
単一責任原則(SRP)
Robert C. Martin。1つのクラスに変更理由は1つだけ。Step 3 の根拠。
Bounded Context
Evans (2003)。同じ用語が同じ意味で通用する範囲が文脈境界。意味が変わる地点に翻訳層を置く。Step 2-B の根拠。
Aggregate / Invariant
Evans (2003)。一貫性を保証する必要のあるオブジェクト群を1つの集約として扱う。Step 2-C の根拠。
Three Examples
Roberts, Johnson。正しい抽象は3つ以上の具体例から帰納する。Step 2-A と Step 7 の根拠。
Parameterize Function
Fowler。似た関数をパラメータで統合する。Step 4 の根拠。
Wrong Abstraction
Sandi Metz (2016)。間違った抽象を共有するより重複を許容する方がコストが低い。Step 4 と反パターン2の根拠。
抽象データ型(ADT)
Liskov, Zilles。抽象データ型は操作によって特徴づけられる。名詞の類似性ではなく操作集合の一致で統合を判断する。反パターン5の根拠。
Team Topologies
Skelton, Pais。モジュール境界と所有チームの一致が、境界の長期的な維持を支える。Step 2-D の根拠。
Modular Monolith
Grzybek ら。境界の成立には公開面・状態分離・通信方式の明示が必要。Step 5 の根拠。