前書き
GRASPパターンの疎結合について、長くなってしまったので、単独での記事にしました。
この疎結合は、パッケージ間の結合という関係だけでなく、
さらに粒度の大きなモジュール、
マイクロサービス同士の結合などにおいても重要な考え方になってくるため、
是非とも新人エンジニアさんから上級エンジニアの方まで、
年数関係なく何度も意識して設計を繰り返す中で、身体にしみこませてほしいものです。
疎結合
「クラス間の関連性と依存性は可能な限り小さくするべきである。」
これはSOLIDのOpen-Closed原則にも繋がってきます。
Open-Closed原則満たすためには、まずは単一責務を満たさないといけない。
これと対応づくように、
【疎結合のためには、情報エキスパートと高凝集パターンが必須】です。
なぜなるべく疎結合にすべきかというと、ある箇所に変更が起きた際に、
できる限りその部分だけの変更だけで済ませたい。
不確実性の高い部分に変更が起きた際の変更コストを極力おさえたい!!
という願望が背景にあり、その1つの手段としてこの疎結合パターンがあります。
したがって、異なる変更理由であるドメインへの変更波紋リスクを抑えたいという
願望が自然に湧き上がるはずです。
そのために、異なるドメインの詳細部分を直接見えるようにせずに、
情報隠ぺいをしてしまい各ドメインの提供する抽象概念でのみ結合する。
これによって疎結合が実現できます。
しかしながら、この抽象概念で結合させることで疎結合を実現しようってのは、
異なるコンテキストとの連携時において言える発想であり、
同じコンテキスト内での疎結合の場合にはいかがでしょうか?
コンポーネント間の間は、インターフェイスで結合させましょう。
たしかにそれはほとんどが正解だと思う反面で、
抽象を使わずとも疎結合を実現できるようなケースだってあります。
下手に抽象概念をかますことで、意図が不透明になってしまうのなら、
抽象を盛り込んだことによってデメリットが上回ってしまいます。
なぜ疎結合にしないといけないのか?
逆に疎結合にはしてはいけないようなケースだってあります。
その違いを分かった上で、ポリモーフィズムのような考え方を導入しないと必ず足元救われる設計になります。
結合度
結合度は合計で7段階ある。
・もっとも結合度が弱い、引数のないメソッド呼び出しの【メッセージ結合】
・次に弱いのが、Inputになる情報内のデータをすべて使って処理を行う【データ結合】
・その次が、Inputになる情報内のデータのうち一部だけを使って処理を行う【スタンプ結合】
これより密な結合は一般的に悪い結合度合いと言われており、推奨されていない結合関係である。
ちなみに、継承の結合関係は、親クラスの変更の影響をすべて諸に子クラスは受けるので、
最も密な結合関係と言えます。
このリスクを考えた上で、Template Methodパターンのような継承を本当に使っていいのか?
という判断をできるようにしましょう。
変更波紋リスクを抑えたい例
たとえば、簡単な例のために、顧客が何か商品を注文するビジネスのラフな例で考えてみましょう。
注文と注文明細という概念が存在する【注文】というドメイン領域と、
顧客の情報が存在する【顧客】というドメイン、商品情報が存在する【商品】ドメイン。
暗算で、これらはそれぞれ違うドメインだとわかりますよね?
(ただし、ビジネスの特性上、データの整合性、たとえば顧客情報を変える際に、
注文情報に記載された顧客の情報が即変更されるような仕様なら、
注文と顧客のドメイン領域は切り離せないです。)
商品情報が変更されたら、顧客の情報まで書き換えないといけないような珍しいビジネスなら話は別ですが、
通常の一般的なビジネスでは、顧客情報と、注文情報、商品情報は互いに異なるドメインです。
よって、顧客と注文のドメインが互いに強く結合しているような状態では、
顧客ドメインと注文ドメインというように、完全にコンポーネントレベルで分離されている状況に比べて、
変更のリスクが跳ね上がってしまいます。
それの代表例が、マイクロサービスへの移行時に、
既存のデータ構造が注文テーブルの注文の主キーが、
顧客の主キーと、商品の主キーのセットへ外部キー制約かつ複合キーになっていると、
たしかに非常に強いデータの整合性は担保できるものの、
静的に密な結合しているがゆえに、データ構造の変更時に関係ないデータドメインまで
影響が出る可能性高いので、変更コストがとんでもないことになり得る。
疎結合にしてはいけない例
しかしながら、注文と注文明細はいかがでしょうか?
注文明細と注文が違うドメインとして切り離されてはいけないはずです。
だって注文明細に書いてある注文日などは、注文の情報と整合していないといけない。
つまり、ここの注文と注文明細の間の関係は強い整合性を担保しないといけないし、
注文と注文明細がビジネス上ライフサイクルを共にするなら、
尚のこと、この注文と注文明細は【強い集約】の関係になる。
このような静的に強い結合関係にあるような場合には、
たとえば「注文日の記載間違えたから修正しないと」となったら、
当然注文明細に書かれた情報も修正しないといけないわけだから、
疎結合であってはならない。すなわち違うドメイン領域に切り分けてはいけない。
継承から委譲に変えるケース
概念モデリング時に、継承関係にしていたけども、
特にサブ概念の方にバリエーションが存在しないようなケースがある。
バリエーションがないってことは、別にバリエーションに応じて振る舞いを変えるという
ポリモーフィズムをする必要なんてないってこと。
そのような場合に、設計上の都合で、変更リスクを抑えるために、
委譲にして結合度合いを少し弱めるケースがある。
バリエーションが増えることが確定しているのなら、ポリモーフィズムを前向きに検討しておいてもいいが、
そうではないのなら継承関係をやめておいた方が身のためであると感じる。
というのも、継承関係って、
親から引き継いでいる全てのロジックに対して
【事前条件】【不変条件】【事後条件】すべてを引き継いでいないと、
リスコフの置換原則に準拠している=継承関係が成立している
とは言えない関係性になってしまうし、
要求の変更などがあった際に、このメンテを考慮し忘れたりしたらたまったもんじゃない。
異なるパッケージとの結合箇所
同じパッケージ内では、クラス同士は密に結合し合っているべきですが、
異なるパッケージとの結合ってなったら話は変わります。
ましてやさらにマクロなサービス粒度などのコンポーネント同士の結合ってなったら尚更。
疎結合ではない例
たとえばの簡単なモデルで考えてみましょう。
まずは使われる側のパッケージ内のクラスのうち3つも4つも5つも参照している状態。
これっていかがでしょうか?
情報隠ぺいができているとは、とても言えない状態ですよね。
あえてそうしているなら別ですが、意図せずに情報隠ぺいできていないと、
使うパッケージ側が意図せずに参照できてしまいます。
これは結果的に、複雑な絡み合いを生み出し、メンテコストを増大させます。
これが非常に安定している状況下ならいいんですが、
コンテキスト境界の位置がハッキリしないような状況下では、望ましくない状態です。
たとえ点線結合だとしても他パッケージとはシンプルに極力1、2箇所でのみとくっついていてほしいもんです。
疎結合である例
では、仮にもしも下図のように、疎結合を実現してくれるような
玄関口の役目を果たしてくれるクラスがあったとしたらどうでしょうか?
このクラスだけがパッケージaに公開されており、
他のパッケージb内のクラスはaに公開されていない状況です。
パッケージaとはこの玄関口一か所とのみ結合している状態なので、
玄関口がない状態と比べて非常に結合状態が把握しやすいのです。
これがデザインパターンのFacadeパターンです。
そう!!! Facadeパターンは、この疎結合パターンの代表例なのです。
まるで緩衝材のような役割になってくれて、
詳細な内部の変更の影響をFacadeちゃんが吸収してくれる形であり、
このFacadeクラスちゃんとのみ、使う側のパッケージは結合しているわけです。
このFacadeパターンも立派な疎結合パターンであること是非覚えておいてほしいです。
インターフェイスを使った疎結合パターン
たとえば、何か業務上のルールが、いくつかのバリエーション持たせて
状況に応じて変えてみたいとかっていうような時がある。
昔のCD、DVD貸出系のビジネスでは、貸出ポリシーは変わらずだけど、
半額キャンペーンの時とか、新作とかについては若干一般作品と、
貸出上のルールが変わっているケースがある。(記憶が正しければ)
つまり、戦略上の概念のポリシーは一定だけども、
状況に応じて、1つ具体なレイヤーの業務戦術のルールがいくつかバリエーションある。
このようなケースで、疎結合でない場合から見てみよう。
疎結合でない例

これはコードで書いている際に、
if文で、「こんな時には戦術ルールA、それ以外では戦術ルールB」みたいな書き方
しているケースである。
これが別に今後、これ以上ルールのバリエーション増えたり、
もしくは、既存のルールに変更が入ることがないよって言うなら別にいい。
前述で記載したようにあくまでも疎結合は、変更リスクを抑えるための手段だからだ。
でももしも、ここに新しく戦術ルールCなどが加わるなどの要求が入るとか、
割と頻繁に戦術ルール自体に変更が入る(Verが新しいものへという言い方がいいかも)
のならば、この構造のままでは本来変更の影響を受けてほしくない、
別のパッケージにあるXにまで影響範囲が広がってしまう。
そして、Xを使う別のパッケージにあるクラスにまで影響は波紋し、、、、、
という事態を引き起こしかねない。
抽象概念をかまして疎結合を実現 - Strategyパターン -
変更コスト抑える欲求の方が上回った場合に、下図のように
インターフェイスをかましてあげることで、疎結合を実現でき、
不確実な変更コストをおさえられる。
これをポリシーパターンと言うらしいが、
振る舞いのバリエーションという意味では、Strategyパターンと一緒なので、
自分はStrategyパターンと呼んでしまっている。
もちろんXを参照しているようなモジュールが何もないとかで、
変更の波紋の最終地点がXとかなら、わざわざ抽象をかます大げさなことをしなくてもいいかもしれない。
しかしながら、今の時代でルールをコロコロ変えたりする必要があるならば、
積極的に選択肢としてこの案を入れた方がいい。
まとめ
ここまでで疎結合パターンについて触れてきました。
有名なのは、情報隠ぺいした上で、抽象概念のところで結合させる例ですが、
それ以外にも、インターフェイスを使わないFacadeクラスのような例や
概念モデル時に継承だったところを委譲に変えるなど、
疎結合を実現できる方法は、色々あることを是非とも覚えておいてください。
くれぐれも疎結合を実現するためには、「抽象概念インターフェイスで結合Yeah!」
みたいに短絡的に考えず、他の方法と比較してみてください。
どの設計案であっても必ずデメリットは存在しますので。