こんにちは。コミュニケーションアプリ「LINE」のモバイルクライアントを開発している石川です。
この記事は、毎週木曜の定期連載 "Weekly Report" 共有の第 55 回です。 LINEヤフー社内には、高い開発生産性を維持するための Review Committee という活動があります。ここで集まった知見を、Weekly Report と称して毎週社内に共有しており、その一部を本ブログ上でも公開しています。(Weekly Report の詳細については、過去の記事一覧を参照してください)
デメテルを知っているか
プログラミング原則の一つに デメテルの法則 というものがあります。この原則では、操作するのは直接知るメンバ(プロパティ/メソッド)だけに限定し、「メンバのメンバ」は操作してはならないということを主張しています。別の表現では「話す相手は友人だけ (only talk to your friends)」と説明されます。より正確に表現すると、メンバにアクセスするときは、そのレシーバ (多くの言語では、ドット演算子.
の左の値) を以下のものに限ります。
this
this
のフィールド/プロパティ- 関数の引数
- 関数内で直接作成されたオブジェクト
- トップレベル/グローバルな値やシングルトン
現実には、レシーバを伴わない関数の呼び出しも、デメテルの法則の法則に従っているとみなしてよいでしょう。
以下は、デメテルの法則に従っている例です。
一方で、以下のコードはデメテルの法則に従っていません。
ここで、以下のようなコードについて考えます。このコードでは、filter
のレシーバが getAllUserModels
の戻り値であるため、デメテルの法則に従っていません。
そこで、ある開発者はデメテルの法則に従った方が良いと考え、以下のようにリファクタリングしました。
この「リファクタリング」に何か問題はありますか?
単なる知人かマブダチか
デメテルの法則は、知識の境界を管理しやすくするため有効です。しかし、デメテルの法則は「知識を分ける」ための手法であり、適用すること自体を目的にしてはなりません。
設計や周辺のコードにもよりますが、今回の例では、呼び出し元の function
内で filter
を直接呼び出しても問題ないでしょう。
デメテルの法則を適用する際には、以下の 2 つのアンチパターンに気をつける必要があります。
- Anti-pattern 1: 表面的な適用をする
- Anti-pattern 2: 呼び出し元/先に過剰な知識を詰め込む
Anti-pattern 1: 表面的な適用をする
先述の「リファクタリング」の例のように、表面的な変更でもデメテルの法則を満たすことができてしまいます。例えば関数を分割し、デメテルの法則に反しているレシーバを引数として渡すだけでも回避できてしまいます。ほかにも、以下のコードのように一旦 this
のプロパティやフィールドに保持することでも、回避可能です。しかし、この方法では、クラスが持つ知識の境界は変わっていません。それどころか、不要な状態を作ってしまっているという新たな問題が発生しています。
デメテルの法則を表明的に適用するのではなく、クラスやモジュールが知るべき/知るべきでない情報は何かを考えることが大切です。
Anti-pattern 2: 呼び出し元/先に過剰な知識を詰め込む
getAllUserModels
の戻り値が常に .filter { it.isFriend }
でフィルタされるのならば、それらをまとめて 1 つの関数にするという方法もあります。以下のコードでは、UserModelRepository
内に getAllFriendModels
を定義することで、デメテルの法則を満たすことができています。
しかし、フィルタの条件が沢山ある状況でこの方法を適用してしまうと、以下のように際限なく関数が増えてしまうかもしれません。
この構造では、新たなフィルタの条件を追加する度に、Caller
と UserModelRepository
の両方を更新する必要があります。「フィルタする」という知識は呼び出し元の Caller
に留めておいたほうが、結果的に単純かつ頑健性の高いコードにできたかもしれません。
デメテルの法則と友だちになるには
デメテルの法則を適用することにより、知識の境界を明確にし、コードの保守性を高められる場合があります。しかし、重要なことはクラスやモジュールが持つべき知識を考慮することであり、法則を表面的に適用することではありません。
では、どのようなときにこの法則を適用できるかというと、典型的にはレイヤードアーキテクチャが対象になります。以下のコードは 3 つのレイヤ (UI, Server, Repository) で構成された例です。ここで、各層が持つ知識を限定するために、 FooViewModel
は FooService
だけに触るべきで、FooRepository
には触れるべきではありません。この場合、デメテルの法則は、レイヤの分割やモジュール化の延長にあるとみなせます。
アーキテクチャが期待通りに実装されているかを確認するためには、Konsist (Kotlin の場合) や ArchUnit (Java の場合) などのアーキテクチャのテストフレームワークを利用するという手段もあります。
一言まとめ
デメテルの法則を表面的に適用するのではなく、知識の境界を意識する。
キーワード: Law of Demeter
, information hiding
, encapsulation