はじめに
前回の記事では、「同じコード」と「同じ知識」の違いに着目して、共通化の判断基準について書きました。共通化すべきかどうかはわかった。でも、正しく分けたはずのクラス同士をどう繋げるかで、また別の問題が起きました。
「このクラス、変更するの怖いな」と思ったことがあります。そのクラスを変えると何が壊れるかわかりません。影響範囲を調べるだけで時間がかかります。結局、既存のコードは触らずに、新しいクラスを横に作って対応していました。似たようなコードが増えていくのはわかっていましたが、壊すよりマシだと思っていました。
この「触ると怖いコード」がなぜ生まれるのか。同じく『オブジェクト指向設計実践ガイド』に答えがありました。原因は依存関係そのものではなく、依存の「方向」です。
依存には方向がある
クラスAがクラスBを使っているとき、AはBに依存しています。Bが変わればAも影響を受けます。ここまでは当たり前の話です。
ポイントは、この矢印の向きによって変更のしやすさがまったく違うということです。本で紹介されていた原則がこれでした。
「自分より変わりにくいものに依存せよ」
依存先が安定していれば、そもそも変わることが少ないので、自分も影響を受けにくくなります。逆に、依存先が頻繁に変わるものだと、そのたびに自分も巻き込まれます。
たとえば、自分が書いたビジネスロジックがRubyの Array や Hash に依存していても、怖くはありません。Rubyの標準ライブラリが突然変わることはまずないからです。でも、先月作ったばかりの社内ユーティリティに依存していたら話は別です。仕様がまだ固まっていないものに依存しているので、変更のたびに影響を受ける可能性があります。
3つの判断軸
依存の方向を判断するために、本では3つの軸が挙げられています。
変わる頻度
変更が少ないものほど、依存先として安全です。本では、変わりやすさには段階があると説明されていました。
- 抽象(インターフェース) — ほとんど変わらない
- フレームワーク・標準ライブラリ — めったに変わらない
- 自分のアプリケーションコード — しょっちゅう変わる
依存の矢印は、この段階の下から上に向けるのが安全です。自分のコードがフレームワークに依存するのは自然ですが、安定すべき共通モジュールが個別画面のロジックに依存していたら危険です。矢印が「変わりやすいもの」に向いてしまっています。
具体と抽象
具体的なクラスより、抽象(インターフェースや「何ができるか」の約束事)に依存する方が安全です。
具体的なクラスは「どうやるか」を全部持っています。税率が変わった、計算の丸め方を直した、パフォーマンスを改善した。こうした変更はすべて具体的なクラスの中で起きます。持っている情報が多い分、変わる理由も多くなります。
一方、抽象は「何ができるか」だけを定義しています。「税額を計算できるもの」という約束事は、中の計算ロジックがどう変わろうと影響を受けません。持っている情報が少ないから、変わる理由も少ない。だから依存先として安定しています。
この違いは、後ほどコード例で具体的に見ていきます。
依存される数
多くのクラスから依存されているものほど、変更したときの影響が大きくなります。これは方向というより「重み」の話ですが、方向の判断とセットで考える必要があります。
10個のクラスから使われている共通処理を変更すれば、最大10箇所に影響が出ます。2個のクラスからしか使われていないものなら、影響は最大2箇所です。同じ「変わりやすいもの」でも、依存されている数によってリスクの大きさがまったく違います。
一番危険な組み合わせ
この3つの軸を重ねると、危険な状態が見えてきます。
「多くのクラスから依存されていて、かつ変わりやすい」もの。これが一番危険です。変更するたびに広い範囲に影響が出て、しかも変更の頻度が高い。冒頭で書いた「触ると怖いコード」の正体がこれでした。
逆に、「多くのクラスから依存されていて、かつ変わりにくい」ものは問題ありません。抽象的なインターフェースやフレームワークがこれに該当します。みんなが使っていても安定しているので、変更の波及が起きにくい。
前回のCSVアップローダーを振り返る
前回の記事で扱ったCSVアップローダーの例を、この観点で見直してみます。
共通化された CsvUploader は、ユーザー設定・部署設定・部署担当者設定などの複数の画面から使われていました。つまり、依存される数が多い状態です。
同時に、各画面のバリデーション要件(メールアドレスの重複チェック、親部署の存在チェックなど)が異なるため、画面ごとの要件変更のたびに CsvUploader 自体を修正する必要がありました。つまり、変わりやすい状態です。
依存される数は多い(複数画面から利用)。変わる頻度も高い(各画面の要件変更で修正が入る)。しかも各画面固有のバリデーションを内部に持っているので、具体度も高い。3つの軸すべてが危険な方向に振れていました。
前回は「同じコードと同じ知識の混同」が原因だと書きましたが、依存の方向で見ても同じ結論に辿り着きます。多くの画面が「具体的で、変わりやすくて、多くから依存されているもの」に向かって矢印を伸ばしていた。これが、触るのが怖かった理由です。
方向を逆転させる
依存の方向が逆になっていることに気づいたとして、どう直すのか。前回の記事で紹介した依存性注入を、方向の観点で見直してみます。
# 具体的なクラスに依存している
class Order
def initialize
@calculator = TaxCalculator.new
end
end
# 抽象に依存するように変える
class Order
def initialize(calculator)
@calculator = calculator
end
end
前回は「違う部分を外から渡す」という視点で依存性注入を紹介しました。今回の観点で見ると、これは依存の方向を変える操作でもあります。Order の依存先が「具体的で変わりやすい TaxCalculator」から「calculate に応答できるもの」という抽象に変わっている。矢印の先が、変わりやすいものから変わりにくいものに逆転しています。
方向が変わると、影響の伝播も変わります。TaxCalculator の仕様が変わっても、Order は影響を受けません。新しい計算ロジックが必要になったら、新しいクラスを作って渡すだけです。
まとめ
依存関係があること自体は問題ではありません。どの方向に依存しているかが重要です。
「変わりやすいものに依存していないか」「多くから依存されているものが変わりやすくなっていないか」。この2つを意識するだけで、変更が怖いコードを生み出すリスクは減らせます。
前回の「共通化すべきかどうか」と、今回の「依存の方向」。この2つは別々の話のようで、根っこは同じだと思います。どちらも「変更の理由が違うものを、不用意に結びつけない」ということです。自分の場合、以前は「どのクラスがどのクラスを使っているか」は意識していても、「その依存先は安定しているか」までは考えていませんでした。共通化の判断基準と依存の方向、この2つの視点を持てたことで、設計時に立ち止まって考える回数が増えた気がします。
参考
- Sandi Metz著『オブジェクト指向設計実践ガイド ~Rubyでわかる 進化しつづける柔軟なアプリケーションの育て方』技術評論社