こんにちは。コミュニケーションアプリ「LINE」のモバイルクライアントを開発している石川です。
この記事は、毎週木曜の定期連載 "Weekly Report" 共有の第 55 回です。 LINEヤフー社内には、高い開発生産性を維持するための Review Committee という活動があります。ここで集まった知見を、Weekly Report と称して毎週社内に共有しており、その一部を本ブログ上でも公開しています。(Weekly Report の詳細については、過去の記事一覧を参照してください)
デメテルを知っているか
プログラミング原則の一つ に デメテルの法則 というものがあります。この原則では、操作するのは直接知るメンバ(プロパティ/メソッド)だけに限定し、「メンバのメンバ」は操作してはならないということを主張しています。別の表現では「話す相手は友人だけ (only talk to your friends)」と説明されます。より正確に表現すると、メンバにアクセスするときは、そのレシーバ (多くの言語では、ドット演算子.
の左の値) を以下のものに限ります。
this
this
のフィールド/プロパティ- 関数の引数
- 関数内で直接作成されたオブジェクト
- トップレベル/グローバルな値やシングルトン
現実には、レシーバを伴わない関数の呼び出しも、デメテルの法則の法則に従っているとみなしてよいでしょう。
以下は、デメテルの法則に従っている例です。
val TOP_LEVEL_VALUE = ...
class Klass {
val property: Property = ...
fun thisFunction() { ... }
fun function(parameter: Parameter) {
// OK: レシーバが `this` の関数
thisFunction()
// OK: レシーバが `this` のメンバ
property.prop
// OK: レシーバが関数の引数
parameter.func()
// OK: レシーバが関数内で直接作成されたオブジェクト
Value(42).prop
// OK: レシーバがトップレベルの変数
TOP_LEVEL_VALUE.func()
}
}
一方で、以下のコードはデメテルの法則に従っていません。
class Klass {
val property: Property = ...
fun function(parameter: Parameter) {
// Bad: レシーバがプロパティのメソッドの戻り値
property.func().prop
// Bad: レシーバが引数のプロパティ
val prop = parameter.prop
prop.anotherProp
}
}
ここで、以下のようなコードについて考えます。このコードでは、filter
のレシーバが getAllUserModels
の戻り値であるため、デメテルの法則に従っていません。
class Caller {
val repository: UserModelRepository = ...
fun function() {
val friendModels = repository.getAllUserModels()
.filter { it.isFriend }
...
}
}
そこで、ある開発者はデメテルの法則に従った方が良いと考え、以下のようにリファクタリングしました。
class Caller {
val repository: UserModelRepository = ...
fun function() {
val friendModels =
filterFriends(repository.getAllUserModels())
...
}
fun filterFriends(allUserModels: List<UserModel>): List<UserModel> =
allUserModels.filter { it.isFriend }
}
この「リファクタリング」に何か問題はありますか?
単なる知人かマブダチか
デメテルの法則は、知識の境界を管理しやすくするため有効です。しかし、デメテルの法則は「知識を分ける」ための手法であり、適用すること自体を目的にしてはなりません。
設計や周辺のコードにもよりますが、今回の例では、呼び出し元の function
内で filter
を直接呼び出しても問題ないでしょう。
デメテルの法則を適用する際には、以下の 2 つのアンチパターンに気をつける必要があります。
- Anti-pattern 1: 表面的な適用をする
- Anti-pattern 2: 呼び出し元/先に過剰な知識を詰め込む