はじめに
「安定」についても知っておいて欲しいと言われたので、まずは調べて理解することにしました。本記事はそのアウトプットです。
ただ、最初に「安定」と聞いたときは、
- 安定している = 良い設計ってことか
- 変更影響を受けにくいということか
とふわっとしたイメージをもちながら、調べていくと、
「多くのモジュールから依存されているほど安定している」
という説明が出てきて、「なんか思ってたのと違う」と思うと同時に
- 安定しているから依存されるのか?
- 依存されるから安定するのか?
という、にわとりたまご論争みたいな疑問が湧いてきたり、
- 依存数が多いだけで「安定」と言えるのか?
- そもそも安定って何のこと
となってきました。
そこで今回は、モジュール設計を理解するための一つの観点として、「安定」 について整理してみます。
最終的には、
- Ca / Ce / Instability
- 安定依存の原則(SDP)
- 安定抽象の原則(SAP)
- 依存性逆転の原則(DIP)
- 高凝集・疎結合との関係
- 変更影響をどう局所化するか
までつなげて整理しいきます。
「安定(Stability)」について理解したい人のお役に立てれば光栄です。
ソフトウェア設計における「安定」とは
まず重要なのは、
安定している = 良い
ではないということです。
ここでいう安定とは、ざっくり言えば、
変更しにくい状態
です。
例えば次のような依存関係があるとします。
A ─┐
B ─┼→ X
C ─┤
D ─┘
A、B、C、DがXに依存しています。
この状態でXを変更すると、Xに依存している複数のモジュールにも影響が出る可能性があります。
そのため、Xは簡単には変更できません。
多くから依存される
↓
変更時の影響範囲が広い
↓
簡単には変更できない
↓
安定している
つまり、
多くのモジュールから依存されているものほど、変更したときの影響範囲が大きくなります。
そのため、設計上は「簡単には変更しにくい=安定している」と捉えます。
「安定」は必ずしもポジティブではない
例えば、システム中の多くのクラスから依存されている巨大なクラスがあったとします。
A ─┐
B ─┤
C ─┼→ GodClass
D ─┤
E ─┘
この GodClass は、多くのモジュールから依存されています。
つまり、変更しにくい。
この意味では「安定しています」。
しかし、だからといって設計として良いとは限りません。
ここでの「安定」は評価ではなく、
そのモジュールがどの程度変更しにくい位置にいるか
という性質を表しているだけです。
そのため、
安定 = 良い
不安定 = 悪い
ではありません。
重要なのは、
そのモジュールの役割と安定度が合っているか
です。
不安定とは何か
反対に「不安定」は、
変更の自由度が高い状態
と考えられます。
例えば、
X → A
→ B
→ C
のように、Xはいくつかのモジュールに依存しているものの、X自身は誰からも依存されていないとします。
Xを変更しても、Xを利用している別のモジュールを壊す心配は比較的小さいため、比較的自由に変更できます。
この意味でXは安定とは逆の状態なので、「不安定」です。
ただし、これも悪いことではありません。
UIや外部サービスとの連携部分のように、もともと変更されやすい領域であれば、不安定であることは自然です。
ここまでを整理すると、
安定
= 変更の自由度が低い
不安定
= 変更の自由度が高い
と考えると分かりやすそうです。
Ca / Ce / Instability
安定性を考えるための指標として、
- Ca
- Ce
- Instability
があります。
Ca: Afferent Coupling
Ca は Afferent Coupling です。
ざっくり言うと、
自分に依存しているモジュールの数
です。
A ─┐
B ─┼→ X
C ─┘
Xについては、
Ca = 3
です。
Caが大きいほど、多くのモジュールから依存されています。
そのため、変更したときの影響範囲が広くなりやすく、変更しにくくなります。
Ce: Efferent Coupling
Ce は Efferent Coupling です。
ざっくり言うと、
自分が依存しているモジュールの数
です。
X → A
→ B
→ C
なら、
Ce = 3
です。
依存先が多いほど、その依存先の変更によってXも変更を余儀なくされる可能性があります。
Instability
CaとCeから、不安定度 I を次のように表せます。
I = Ce / (Ca + Ce)
I は0〜1の範囲になります。
I = 0
→ 安定側
I = 1
→ 不安定側
例えば、
A ─┐
B ─┼→ X → Y
C ─┘
なら、
Ca = 3
Ce = 1
なので、
I = 1 / (3 + 1)
= 0.25
となり、比較的安定したモジュールだと考えられます。
感覚的には、
Caが大きい
→ 多くから依存される
→ 自由に変えにくい
Ceが大きい
→ 多くに依存する
→ 周囲の変更に影響されやすい
と考えると理解しやすいです。
安定依存の原則(SDP)
SDPは Stable Dependencies Principle の略で、日本語では「安定依存の原則」と呼ばれます。
考え方は、
依存は、不安定なものから安定したものへ向ける
というものです。
例えば、
A → B
なら、
I(A) >= I(B)
であることが望ましい、ということです。
つまり、依存先の方が同じか、より安定している状態です。
なぜ「安定 → 不安定」が問題なのか
例えば、注文処理を担当する OrderService がStripeのSDKに直接依存していたとします。
class OrderService
def checkout(order)
Stripe::PaymentIntent.create(
amount: order.total_price,
currency: "jpy"
)
end
end
依存関係は、
OrderService
↓
Stripe SDK
です。
OrderService が複数のControllerやBatchから使われているとすると、比較的安定したモジュールになります。
一方でStripe SDKは、
- API仕様変更
- SDK変更
- パラメータ変更
- 別サービスへの乗り換え
など、外部都合で変更される可能性があります。
すると、
Stripe側の変更
↓
OrderServiceの変更
となります。
安定させたい業務ロジックが、変わりやすい技術詳細に引っ張られている状態です。
SDPでは、こうした依存を避けようと考えます。
安定抽象の原則(SAP)
そこで出てくるのが Stable Abstractions Principle です。
SAPでは、
安定しているモジュールほど、その安定度に応じて抽象的であるべき
と考えます。
安定したモジュールは変更しにくいため、具体的な実装で固定しすぎると将来的な変更が難しくなります。
例えば、
PaymentGateway
という抽象を用意します。
OrderService は、
「決済できる」
という契約だけに依存します。
OrderService → PaymentGateway
Stripe固有の処理は、
StripePaymentGateway
に閉じ込めます。
OrderService → PaymentGateway ← StripePaymentGateway
抽象を挟むだけで安定するわけではない
ここで疑問になるのが、
抽象を1枚挟んだだけで、本当に変更に強くなるのか?
ということです。
当然ですが、interfaceやクラスを1つ増やすだけで、自動的に良い設計になるわけではありません。
重要なのは、
誰の都合でその契約が定義されているか
です。
例えばStripe SDKのAPIは、
Stripe::PaymentIntent.create(...)
のように、Stripe側の都合で定義されています。
一方、
payment_gateway.pay(amount)
という契約は、
注文処理にとっては、金額を渡して決済できればよい
という業務側の要求から定義できます。
つまり、
Stripeの都合
ではなく、
注文処理が必要としている機能
に依存できるようになります。
DIP
DIPは Dependency Inversion Principle です。
典型的には、
上位モジュールと下位モジュールの両方が抽象に依存する
という考え方です。
直接依存すると、
OrderService
↓
StripePaymentGateway
つまり、
業務ロジック
↓
技術的詳細
という依存になります。
これを、
PaymentGateway
↑ ↑
OrderService StripePaymentGateway
という形にします。
これによって、OrderService はStripeという具体的な技術詳細を知らなくて済みます。
なぜ「知らない」方がいいのか
設計において、知識 も依存です。
OrderService が、
Stripe::PaymentIntent
Stripe APIのパラメータ
Stripe SDKの仕様
Stripe固有の例外
まで知っていると、それらが変更されたときに OrderService も影響を受けます。
一方、
payment_gateway.pay(amount)
だけ知っていれば、
OrderService が知っているのは、
「金額を渡せば決済できる」
ということだけです。
そのため、
Stripe API変更
↓
StripePaymentGatewayを変更
という形に変更を閉じ込めやすくなります。
最終的に目指したいのは変更影響の局所化
ここまで「安定」について見てきましたが、「凝集」や「結合」などと同様で最終的に、
変更影響を局所化する
という考え方につながっているように感じました。
良い設計だからといって、変更がなくなるわけではないので、
重要なのは、
変更が起きたとき、どこまでコードが揺れるか
例えば、
Stripe API変更
↓
StripePaymentGatewayだけ変更
で済むなら、変更を局所化できています。
一方で、
Stripe API変更
↓
OrderService
↓
Controller
↓
Batch
↓
その他...
まで広がるのであれば、変更影響が大きい設計と言えそうです。
知ってるだけじゃなくて、使える観点として蓄えておきたい
こうした考え方は、知識として知っているだけでなく、コードを書くときやPRレビュー、AIが生成したコードのセルフチェックにも使えるようにしてこそだと思います。
以下のような問いを投げかけていこうと思います。
- このクラスの責務を1文で説明できるか?
- このクラスは相手の詳細を知りすぎていないか?
- もし、このクラスを変更するとしたらどんなときか?
- 外部仕様が変わったら、何ファイル変更する必要があるか?
- 変わりにくくしたいものが、変わりやすいものに依存していないか?
- この抽象は何の変更から守るために存在しているか?
参考文献

