LINEヤフー Tech Blog

LINEヤフー株式会社のサービスを支える、技術・開発文化を発信しています。

コード品質向上のテクニック:第55回 デメテルを知っているか

こんにちは。コミュニケーションアプリ「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: 呼び出し元/先に過剰な知識を詰め込む

Anti-pattern 1: 表面的な適用をする

先述の「リファクタリング」の例のように、表面的な変更でもデメテルの法則を満たすことができてしまいます。例えば関数を分割し、デメテルの法則に反しているレシーバを引数として渡すだけでも回避できてしまいます。ほかにも、以下のコードのように一旦 this のプロパティやフィールドに保持することでも、回避可能です。しかし、この方法では、クラスが持つ知識の境界は変わっていません。それどころか、不要な状態を作ってしまっているという新たな問題が発生しています。

class Caller {
    val repository: UserModelRepository = ...
    var allUserModels: List<UserModel> = listOf()

    fun function() {
        allUserModels = repository.getAllUserModels()
        val friendModels  = allUserModels.filter { it.isFriend }

        ...
    }
}

デメテルの法則を表明的に適用するのではなく、クラスやモジュールが知るべき/知るべきでない情報は何かを考えることが大切です。

Anti-pattern 2: 呼び出し元/先に過剰な知識を詰め込む

getAllUserModels の戻り値が常に .filter { it.isFriend } でフィルタされるのならば、それらをまとめて 1 つの関数にするという方法もあります。以下のコードでは、UserModelRepository 内に getAllFriendModels を定義することで、デメテルの法則を満たすことができています。

class Caller {
    val repository: UserModelRepository = ...

    fun function() {
        val friendModels = userModelRepository.getAllFriendModels()

        ...
    }
}

class UserModelRepository {
    fun getAllFriendModels(): List<UserModel> { ... }
}

しかし、フィルタの条件が沢山ある状況でこの方法を適用してしまうと、以下のように際限なく関数が増えてしまうかもしれません。

class UserModelRepository {
    fun getAllFriendModels(): List<UserModel> { ... }
    fun getAllOnlineUserModels(): List<UserModel> { ... }
    fun getAllOnlineAndNonFriendModels(): List<UserModel> { ... }
    fun getAllInvitingModels(): List<UserModel> { ... }
    fun getAllUserWithCustomPictureModels(): List<UserModel> { ... }
    ...
}

この構造では、新たなフィルタの条件を追加する度に、CallerUserModelRepository の両方を更新する必要があります。「フィルタする」という知識は呼び出し元の Caller に留めておいたほうが、結果的に単純かつ頑健性の高いコードにできたかもしれません。

デメテルの法則と友だちになるには

デメテルの法則を適用することにより、知識の境界を明確にし、コードの保守性を高められる場合があります。しかし、重要なことはクラスやモジュールが持つべき知識を考慮することであり、法則を表面的に適用することではありません。

では、どのようなときにこの法則を適用できるかというと、典型的にはレイヤードアーキテクチャが対象になります。以下のコードは 3 つのレイヤ (UI, Server, Repository) で構成された例です。ここで、各層が持つ知識を限定するために、 FooViewModelFooService だけに触るべきで、FooRepository には触れるべきではありません。この場合、デメテルの法則は、レイヤの分割やモジュール化の延長にあるとみなせます。

// UI layer
class FooViewModel(private val service: FooService) : ... {
    fun layout(...) {
        // Touches `FooService`, but never `FooService.repository` 
        val fooValue = service.getFooValue()
        ...
    }
}

// Service layer
class FooService(private val repository: FooRepository) : ... {
    fun getFooValue(): FooValue {
        val fooResult = repository.queryFooValue()
        ...
    }
}

// Repository layer
class FooRepository {
    fun queryFooValue(): ApiResult<FooValue> { ... }

    ...
}

アーキテクチャが期待通りに実装されているかを確認するためには、Konsist (Kotlin の場合) や ArchUnit (Java の場合) などのアーキテクチャのテストフレームワークを利用するという手段もあります。

一言まとめ

デメテルの法則を表面的に適用するのではなく、知識の境界を意識する。

キーワード: Law of Demeter, information hiding, encapsulation

コード品質向上のテクニックの他の記事を読む

コード品質向上のテクニックの記事一覧