デメテルの法則

「ドットがつながったらあかんで」とかいうやつ、くらいの認識しかなかったが整理できたのでメモ。

結論としては、「ドットがつながったらあかんで」ではなく、「ドットがつながったときは不要な依存をつくっちゃう場合があるから、ほんとにそれでいいかよく考えなね」であると理解した。

飛び越えた依存

例えば以下のようなコードを考える。 (以下、barや@barはBarインスタンスであることを前提とし、bazについても同様。)

class Foo
  def foo_method
    @bar.baz.baz_method
  end
end

あるいは、こんなコード。

class Foo
  def foo_method(bar)
    bar.baz.baz_method
  end
end

Fooの中で@bar/barに対してBar#bazというメソッド呼び出しをしているので、FooはBarのインターフェースを知っている=FooはBarに依存していることになる。

これは以下のように図示できる。

Foo -> Bar

ではBarとBazの関係はどうだろうか。

ここでは、Bar#bazがBazインスタンスを返している。

そのばあい、確証はないけどBarはBazインスタンスに対してあれこれメソッド呼び出しをすることが多い。

(たとえばCustomer#walletがWalletインスタンスを返す場合、CustomerはCustomer#payの中でWallet#withdrawを呼び出したりする。)

このとき、BarはBazに依存している。

Foo -> Bar -> Baz

そんで、上記の例だとFooはBaz#baz_methodを呼んでいるので、FooはBazのインターフェースを知っている=依存している。

Foo -> Bar -> Baz
  |            ^
  |____________|

FooはBarを飛び越えてBazにも依存しているわけだ。

それで、どうする?

この飛び越えた依存を解消するためにBarにメソッドを生やす、というやり方は明らかなので示さない。

(よくみるdelegateもつまりはBarにメソッドを生やす1つの方法だ。)

ここで問いたいのは、上記の飛び越えた依存が「解消すべきものなのかどうか」だ。

答えは、場合による。

飛び越えた依存を解消すべき場合

こちらの記事のPaperBoyの例は、飛び越えた依存を解消すべき場合だ。

依存関係を先程の図に当てはめると以下のようになる。

PaperBoy -> Customer -> Wallet
  |                       ^
  |_______________________|

これらのモデルによって表現されている現実においては、PaperBoyはWalletのことを知る必要はない。

よって、「PaperBoyはWalletのことを知らない=依存しない」形の設計によって現実を適切にモデリングできると判断できる。

不要な依存関係はつくらない方がよいし、それに「依存関係の不在」も含めてなるべく現実に近いモデリングをしておいた方が、今後現実に生じる出来事をモデルの中で表現しやすくなる。

だからこうすべきなのだ。

PaperBoy -> Customer -> Wallet
飛び越えた依存を解消すべきではない場合

上述の記事の後半に記載されているのは、必ずしも依存を解消すべきとは限らないことの良い例だ。(以下ではクラス名に多少の改変を加えている。)

OrdersCotroller -> Order -> Customer
  |                             ^
  |_____________________________|

ユースケース層に属しているOrdersControllerにとっては、OrderとCustomer両方のインターフェースを知っている=依存しているのはごく自然なことだ。

むしろ、モデルたちを並列的に扱うことが期待されるコントローラが、Customerに対する操作を逐一Order経由で行うことの方が不自然である。

デルメルの法則は私たちに「本当にその飛び越えた依存が存在していいの?」と問いかけるが、この場合は「もちろん。その依存関係は許容されるものだ」と胸を張って答えればいい。

デルメルの法則は絶対に従うべき掟ではなく、不注意な過ちを防ぐための注意書きに過ぎない。


もう1つ例を挙げよう。

Order -> Customer -> String
  |                    ^
  |____________________|

たとえばOrder#summaryの中でcustomer.name.upcaseなどを呼ぶ場合に、上記の依存関係が発生する。

この場合、Customer#name_in_upcaseを定義して、飛び越えた依存を解消すべきだろうか?

いや、OrderがRubyの組み込みクラスであるStringのインターフェースを知っているのはごく自然なことなので、この「飛び越えた依存」はあって然るべきものだ。

より実際的に言うならば、Stringは十分に安定的であり変更可能性が小さいので、その僅かなリスクのためにCustomer#name_in_upcaseを生やすというコストを払うのは賢明ではない。

総括

デメテルの法則は「こういう場合には注意した方がいいよ」というだけで、何をすべきかを指示するものではない。

注意したうえでどう判断するかは、状況次第である。

しかも、上記の例では依存を解消すべき/すべきでないことが明らかな例を挙げたが、そうした判断は必ずしも自明ではない。

「FooがBazを知っている形の設計にする」か「FooがBazを知らない形の設計にする」かは、多くの場合「そのドメインをどのようにモデリングするか」という、"決め"の問題になることも多いように思う。

以上適当なメモなので誤りなどあればご教示ください。