k-tokitoh

2020-04-25

デメテルの法則

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

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

飛び越えた依存

例えば以下のようなコードを考える。 (以下、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

飛び越えた依存を解消すべきではない場合

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

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 を知らない形の設計にする」かは、多くの場合「そのモデリングするか」という、“決め”の問題になることも多いように思う。

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