背景
ソフトウェアアーキテクチャハードパーツの5章にこのパターンが記載されている。
このコンポーネントベース分割パターンは、粒度がぐちゃぐちゃになってしまった要素同士の粒度を揃えつつ、最終的にプロセス面において適切にコンテキスト境界を定義し、モジュラーモノリスの形を目指す上でのリスクを抑えたステップである。
端的に言ってしまえば
AsIs:コンポーネントのサイズ感はぐちゃぐちゃで重複概念も存在する
ToBe:プロセス側面でコンテキスト境界によって疎に分けられたモジュラーモノリス構造
までの移行計画ステップそのものが、このパターンで書かれている内容である。
前提
1つのサービスは1つ以上のコンポーネントから構成される。
SLAP原則・・・抽象度の粒度を揃えましょうっていう原則
戦術的フォーク
こちらとは別に戦術的フォークというものもある。
ただしこちらの戦術的フォークは、極論無駄なものを削っていくだけに留まるので、
ドメインに対する理解が向上し適切なコンテキスト境界を定義するには不十分。
それでも無駄なものを削ぎ落すことによって認知負荷を減らし、
その上でこのコンポーネントベース分割をした方がいいと感じる。
よって、ハードパーツ書籍ではコンポーネントベース分割と戦術的フォークのトレードオフが考察されていたが、自分は状況に応じて両方をやった方がいいと思っている。
というか認知負荷がかかるほど肥大化する前に、継続的に要らないものを戦術的フォークで削って、コンポーネントベース分割を継続的に行うっていうのを常識にするっていう風にしておいた方が無難。
拡張する前に無駄なものを削れ!! である。
理由はECRS原則の思想を私自身が取り入れているからである。
書籍ほどご丁寧でなくてもいい
書籍では超丁寧なステップで書かれていたが、実際には継続的にモジュラーモノリスを維持する文化が組織に浸透しているなら、ここまでご丁寧にやらなくてもいい。
大事なのは粒度を合わせつつ、コンテキストマップを常に更新し続けることだ。
おそらく紹介されているステップは、相当に泥団子化したビッグバーンスケールのものを壊さずにリスクを最小限に抑えつつ、モジュラーモノリスにしていくまでの工程という意味合いである。
本来はそんなひどい状態になるよりも前に、リアーキテクティングするべきであるし、そのような文化を形成しなくてはまた再発する。
組織構造の見直し
このコンポーネントベース分割をアプリケーションアーキテクチャ層で行ったら、
必ず逆コンウェイ的にビジネスアーキテクチャ側も更新しよう。
更新し忘れによる、システムとビジネスの乖離は、せっかく内部構造をメンテしたシステムや組織全体を崩壊に導きかねない。

では、次から早速モジュラーモノリスまでの移行ステップを紹介する。
①コンポーネント特定とサイズ調整パターン
まずはこのステップでどんなコンポーネントがあるかを特定し、そのサイズ感としてあまりに大きいものを分割するなどの調整を行う。
名前空間識別子による構造管理
先に図のようなディレクトリ構造が分かるような図を描いたうえでやった方がいい。
ラフな図も描かずにいきなりコード書くのは自ら構造がぐちゃることに飛びつくようなもの。
こうしておけば、panda.customerとpanda.staffは同じ粒度なんだと名前を見て判断できるので、
名前空間の識別子に一貫性を持たせておく。
くれぐれもpanda.Address panda.Nameなどのようにどこのグループに所属しているのかわからないとか、
panda.Addressと書いてる一方で、panda.staff.Nameと書いていたりするという一貫性のないものは
「え? AddressとNameって同じ粒度じゃないの? ん??」と混乱を招くので、
設計方針として
【名前空間に対して、コンポーネントはpanda.頭文字小文字名前、クラスファイル名は
panda.頭文字小文字のコンポーネント名.頭文字大文字クラス名 と書くことをルールとする】という規則を持たせておくことが、SLAP原則違反のリスクを緩和してくれる。
パーセント

これによって、定性的にそのコンポーネントの大きさが大きすぎるのか?小さすぎるのか?特定可能。
これは上図のような3つくらいしかコンポーネントがない状況下ではそほど恩恵はないかもしれないが、
全体のコードに対して、そのコンポーネントが大体どのくらいの割合を占めてるかを数値化してくれる。
上図ではわかりやすくするためにあえてコンポーネントの大きさを変えてみた。
たとえば全体に対してpanda.customerが50%、panda.staffが35%、panda.babyが15%を占めていたとする。
ザックリとした計算だが、3つのコンポーネントからこのpandaが構成されてるのなら、
大体各コンポーネントの大きさは、33%程度であってほしい。
ズレていたとしてもせいぜい±5%程度であってほしいので、28%~38%の間なら許されるとしよう。
panda.staffは2%のズレしかないから許容範囲内だが、panda.customerとpanda.babyは明らかに許容範囲から逸脱している。
そのためここもリファクタリング活動の対象になり得る。
ステートメント数とファイル数
ステートメント・・・ソースコード内で実行される1つの完全なアクションのことで、Javaとかであれば【;】で区切られる。
このステートメント総数が、対象のコンポーネントがどの程度の役割を担ってるのか?
どのくらい複雑化が定性的に分かる指標の1つ。
たとえば対象コンポーネントのステートメント総数が10000とかあるのに、
4つのクラスファイルしかないとかであれば、
定性的に判断しても明らかに神クラスの匂いがプンプンなので、小さくリファクタリングしないと!とかって考えられる。
適応度関数
せっかくこのステップでコンポーネントのサイズ感を調整しても、
組織文化として綺麗な設計を意識しなくてもいい風習や、
開発者たちの設計スキル不足といった根本原因が解消されていないようでは、
またサイズ感がぐちゃぐちゃになってしまう。
そこで適用度関数のように自動でモニタリングしてくれる仕組みを設けることで事前に対処しておく。
まあ本当は、システムが巨大な泥団子になるよりも前にアーキテクトや開発者たちが、意識しなくてもサイズ感が許容範囲を超えないように適用度関数を定義すべきだとは思う。
もちろんプロダクトライフサイクルの初期段階では、わざわざ適用度関数を定義しても費用対効果が薄いケースが多いと感じるので、あえてやらなくていいとは思う。
しかしながら徐々に成長期に入ってビジネスが拡大してきたら、
アーキテクチャテストなどの仕組を事前に盛り込んでおいた方が無難であろう。
②ドメイン共通コンポーネントの収集パターン
各ドメインで再利用されている重複概念を1つにまとめて分離する。
コンポーネント粒度のDRY原則ともいえる。
ここで共有サービスもしくは、共有ライブラリとしてまとめてしまう。
共有サービスか共有ライブラリのどちらにするかはまた別の記事で触れます。
この時に重要な設計の考え方として、コンポーネントの再利用観点の凝集原則がある。
抽象化してまとめる
たとえば3つのコンポーネント
①顧客通知コンポーネント
②チケット通知コンポーネント
③アンケート通知コンポーネント
という具体なレイヤーで視れば確かに違いはあれども、
いずれも1段階抽象化すれば顧客へ通知するという点で共通であると見なせる場合があったとする。
図にするとこんな感じだ。
下図の各ブロックはコンポーネントである。クラスの粒度ではない。

通知コンポーネントという抽象なコンポーネントのAPI定義としては、
①~③の具象コンポーネントのAPIの事前事後を
全て満たすように、リスコフ置換原則を満たすように設計することが求められる。
そうすることで、他のコンポーネントと具象コンポーネントとの結合が複雑であった場合に、
上図のようにシンプルにすることができる。
これは疎結合の思想そのものであり、コンポーネントレベルのStrategyパターンともいえると感じる。
ただし注意が必要である。
もしも具象コンポーネントと他のコンポーネントとが複雑に絡み合っていない場合には、通知コンポーネントのような抽象概念を導入するだけ複雑になってしまうリスクがある。
③コンポーネントのフラット化パターン
ここでは、コンポーネントと他のコンポーネントとの粒度を揃えるフェーズである。
フラット化されて、ディレクトリ構造として定義されていることを保証する目的でこのパターンを使用する。
要は静的構造面でのSLAP原則をコンポーネントというマクロなレベルにおいて適用するのである。
自分の体験した案件でもあったが、パッケージ粒度とそれより2段階くらいマクロな粒度の要素が同じレベル感として扱われているなんてことがある。
それはディレクトリ構造を全く意識されておらず、名前付けも適当であったこと。
さらには縦割りによるコラボレーションのなさにより、
横断した他のビジネスに対しても粒度を揃える意識の低下が主な原因だと思われる。
定量的見分け方
コンポーネント名 | 名前空間 | ステートメント数 | ファイル数 |
---|---|---|---|
チケット | ss.ticket | 7009※ | 45※ |
チケット割当 | ss.ticket.assign | 7845 | 14 |
チケット配信 | ss.ticket.route | 1468 | 4 |
アンケート | ss.survey | 2204※ | 5 |
アンケートテンプレート | ss.survey.templates | 1672 | 7 |
コンポーネントのステートメント数やファイル数などからも大体類推することも可能。
同じ粒度なのに、なぜか異様にファイル数が多いor少ないものとかはもしかしたらレベル感が違うかもしれない。
多い場合には、そのコンポーネントの責務が多重責務になってしまっているとか。
敢えてそうしているとかならいいが、そのコンポーネントにまだ変更が入りそうな不安定フェーズにおいては、極力1つの責務にした方がいいであろう。
そして他のコンポーネントと粒度が揃っているのか?
ステートメント数やファイル数などを見て定量的にチェックするのだ。
勿論、この定量分析によって粒度が揃っていない臭い部分を発見する手立てにはなるが、数字的に問題ないからといって 粒度がフラットであるとは言い切れない。
そこだけは注意しよう。
補足 -チームの認知負荷-
チームトポロジー的な話の観点もここでの考察にはあった方がいいと感じる。
というのも、どんなに定量的に見てフラット化されていたとしても、
そのコンポーネントを担当するチームがまだ認知負荷のキャパが広くない場合には、
そのコンポーネントをさらに分割統治して、1つ以上のサブコンポーネントにして認知負荷を減らす方向にした方がいい。
また認知負荷のキャパが広くないわけだから、当然サブコンポーネント同士の境界が曖昧であるような箇所、つまりコラボレーション連携を必須とするようなコンポーネントは担当させるべきでない。
④コンポーネント同士の依存関係判断パターン
この工程ではサービスよりも1段階詳細なコンポーネントに対するコンテキストマップを作成すると思ってもらえればいい。
作成した静的なコンテキストマップの図を見れば、
コンポーネント同士が静的に結合している箇所があったりしても発見しやすく、
そおれによってマイクロサービスとして無理に分けるなんて間違った判断しないで済む。
依存関係の数を制限する適用度関数
すべてのコンポーネントが他のコンポーネントと複雑な結合にならぬよう、
どのコンポーネントも一定数以上の依存関係持たないように統制をはかる仕組みを設ける。
これは不必要な他のチームとの連携を制限することで、生産性を向上させることにも貢献するといえるであろう。
特定のコンポーネントへの依存を制限する適用度関数
これは変化しやすい安定していないコンポーネントへの依存を禁止することで、
不安定コンポーネントの変更リスクから保護するためにもあった方がいい。
多くの場合には、依存関係の制限ごとに1つの適用度関数を定義するため、
10種類のコンポーネント制限があった場合には、10個の適用度関数が存在する。
そりゃそうだ? だって1つの適用度関数で1つの制限目的を果たしたいのだから。
1つの適用度関数に意図が複数存在してたら、可読性も悪い。
⑤コンポーネントドメインの作成
④で作成したものをもとに、関係しあったコンポーネント群をグループ化する。
④で作成した各コンポーネント同士の結合度に注目することで、このグループ化ができる。
そうすることで1段階マクロに見たドメインサービス粒度でのコンテキストマップを作成するのだ。
ドメインサービスの粒度には、④で考えたコンポーネントが1つ以上含まれている。
ようはドメインサービスとコンポーネントの関係は、パッケージとクラスの関係性と一緒の構造関係である。
⑥ドメインサービスの作成パターン
⑤までくればあとは簡単だ。
データベース部分でのみ各ドメインサービスはケツぐしている状態を創るのだ。
モジュラーモノリスの疎に結合したモジュールを定義するのである。
ここまで来た際には、もうこの時の境界付けられたコンテキストの位置が把握できているので、開発者たちのドメインに対する理解もだいぶ深まっているはずである。
わたしの見解
書籍ではプロセスの観点でまずはコンテキスト境界を見つけていく過程を6ステップに分けて紹介されていた。
しかしながら私が思うのは、プロセスとデータの側面の行き来の考察も重要であると感じる。
勿論、膨れ上がったビッグバーンスケールの泥団子をいきなりプロセスとデータの側面の行き来をするというのは非常に骨が折れる作業。さらに集中力が続かないから、まずはプロセスの面でのコンテキスト境界から考えるというのは賛成である。
しかしながら基本的にマイクロサービス化が失敗する要因として
・プロセスの側面でのコンテキスト境界の考察の不十分さ
・データの側面でのコンテキスト境界の考察の不十分さ
・コンウェイ力学を無視して組織構造を相似形にしていない
これら3つに大体集約される以上、徐々にデータとプロセスの側面を行き来できるようにならなくてはいけないと感じる。
同じモジュラーモノリスでも、データがすべて密に結合したままの状態と、
データコンテキスト境界(データドメインという)ごとに論理的に切り分けられている状態とでは、わかりやすさは雲泥の差であると感じる。
特に泥団子の大きさが大きければ大きいほど。
だからといって、プロセスを分割したらすぐにデータも分割せよってことではない。
データ構造はプロセスの分割以上にコストもかかるし、戻したくなった際のリスクもあり得るので。
一旦頭のなかだけでも、論理的に分割してみてってことだ。