デメテルの法則(Law of Demeter, LoD)と葛藤
「あるオブジェクトは、自分が直接知っているオブジェクトにしかメッセージを送ってはいけない」という規則。
つまり「友達の友達に話しかけない」という表現をされることも、、、、なるほど?
// ショッピングサイトで、顧客の注文品目数を知りたいケース
// ❌ デメテルの法則違反した実装の場合
// (customer)から取得した(order)から取得した品目一覧(items)のサイズを取得する
var amount = customer.getOrder().getItems().size();
// ✅ デメテルの法則守った実装の場合
// 顧客に注文した品目数の取得させる
var amount = customer.getOrderTotalAmount();
この例だとcustomer
は「友達」で、order
は「友達の友達」に当たるわけですね。
デメテルの法則を守った場合の実装では、customerのパブリックメソッドとして定義され、order
やitems
のオブジェクトの存在を意識しなくても欲しい情報が得られます。
違反している実装より良くみえますね。
では以下のような場合はどうでしょうか?
// デメテルの法則を守る必要があるのか?
// ❓ コレクションの走査処理
orders.stream().map(...).filter(...)
// ❓ 外部API取得結果の解析
data.getBody().getProduct().name
// ❓ アカウント情報の表示
customer.getProfile().getAccount().id;
メソッドが数珠繋ぎに呼び出されていて、同じくデメテルの法則を違反しています。デメテルの法則を守るため、別のメソッドに処理を委譲する必要があるのでしょうか?
- コレクションの走査処理
- ✅ 問題なし
- オブジェクトの抽象度が同じであれば、連鎖しても問題ない
- 外部API取得結果の解析
- ✅ 問題なし
- DTOなど、振る舞いを持つことが考えられない、単純なデータ構造では不要
- アカウント情報の表示
- ⚠️ 問題ないときもある
- → こちらについて考える
なぜ、このように考えることができるのでしょうか?
「法則」だけど絶対ではない
「デメテルの法則が「法則」になったのは、人間がそう決めたからです。この大げさな名前にだまされてはいけません。法則といっても、これは「毎日歯を磨くべきです」といった規則のようなもので、万有引力の法則とは異なります。歯医者には言いにくいかもしれませんが、たまに破ったところで、世界が崩壊することはありません。」
—『オブジェクト指向設計実践ガイド ~Rubyでわかる 進化しつづける柔軟なアプリケーションの育て方』Sandi Metz著
今回考えるのは、デメテルの法則は「必ず守らないといけないものではない」という主張についてです。
そもそもなにが嫌なのか
「毎日歯を磨くのはなぜ?」と聞かれれば、「虫歯や歯周病になるリスクが高まるから」と答えるかもしれません。
では、「デメテルの法則を守るのなぜ?」ときかれればどうでしょうか。
一言でいえば「他のオブジェクトから取得したオブジェクト(=友達の友達)の内部構造や振る舞いに依存すると、変更コスト上昇リスクが高まるから」です。
例
先ほどの例で、仕様変更によって、Customerから取得していたOrderクラスの振る舞いが変わるとどうなるでしょうか。
例えば、OrderクラスはOrderLineクラス(注文明細クラス)に依存した構造へ変わった時、どのようは。
// 仕様変更: 数量とセットになった注文明細(OrderLine)で注文内容を管理するようになった
// ❌ デメテルの法則違反した実装の場合
// 顧客(customer)から取得した注文(order)から取得した明細一覧(orderLines)の要素の数量を合計する
- var amount = customer.getOrder().getItems().size();
+ var amount = customer.getOrder()
.getOrderLines()
.stream()
.mapToInt(OrderLine::getQuantity) // 各明細(OrderLine)から数量(int)を抜き出す
.sum();
// ✅ デメテルの法則守った実装の場合 (変更なし)
var amount = customer.getOrderTotalAmount();
❌デメテルの法則違反した実装の場合
OrderクラスはOrderLineクラス(注文明細クラス)に依存した構造へ変わった時、デメテルの法則に違反しているコードでは、注文明細から数量を抽出して合計するコードを書くことになりました。
✅デメテルの法則守った実装の場合
Customerの中で扱われるOrderクラスを「知らない(参照しない)」ので仕様変更の影響は受けていません。呼び出し元は今まで通り、customer.getOrderTotalAmount();
を呼び出せば良いのです。
デメテルの法則が守られていたことによって、「変更コストの上昇リスクを回避できていた」といえます。
(もちろん、CustomerクラスはOrderクラスの仕様変更影響を受けます。)
破るという判断について
// ❓ アカウント情報の表示
customer.getProfile().getAccount().id;
これまでの流れでいえば、以下のコードはデメテルの法則の法則違反であり、customerにはgetAccountId()
のようなパブリックなメソッドを作成して処理を委譲すべきと言う判断ができそうです。
リスクがコストに見合うのか
デメテルの法則に則るための実装コストより、「変更コストの上昇」リスクが高ければ、パブリックなメソッドに委譲する実装を進めていく方が良いかもしれません。
例えば以下のような 「変更コストの上昇」リスクが低い状況 であれば、そのままにすることも検討できます。
- 参照元がとても少ないオブジェクト
- リスクが顕在化しても、変更コストは低い
- 構造や振る舞いの変更頻度が少ない安定感抜群のオブジェクト
- そもそも変更されず、リスクが少ない
- 同じモジュールないの参照に簡潔している
- 影響範囲が広くても、変更や依存関係が単純で、コスト低い
つまり、customer.getProfile().getAccount().id;
が、局所的に使用されていたり、安定しているオブジェクトと判断できる場合、メソッドを定義せず呼び出すという判断は妥当とも考えられます。
まとめ
デメテルの法則は絶対なルールではなく、あくまでコードの潜在的リスクを炙り出すためのサインのようなもの。
デメテルの法則がくれるサインは
「オブジェクトのパブリックメソッドを定義することによって、変更コストを下げる余地が残っているのではないか?」
と考えることもできます。
パブリックメソッドによってオブジェクト間の境界線が明確になり、オブジェクトの目的や責務も整理できるかもしれません。
現場で役立てる
実際の現場ではこの法則をどう役立てていくべきでしょうか?
a.b().c()
のようなメソッドが数珠繋ぎになったコードを見つけたら、一目散に、a
にパブリックメソッドを作成し、処理を委譲すれば良いのでしょうか?
いいえ。
開発者は、デメテルの法則のサインを受け取って、メソッドへの委譲を検討し、コストがリスクに見合うのかを判断することです。