0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

デメテルの法則。メソッドチェーン地獄の解消から、ドメイン駆動設計のコンテキスト境界まで

Posted at

1. はじめに:デメテルの法則とは

デメテルの法則(Law of Demeter)とは、1987年にマサチューセッツ工科大学のDemeterプロジェクトで提唱された「最小限の知識の原則(Principle of Least Knowledge)」です。
オブジェクト指向設計において、あるオブジェクトが知ってよい他のオブジェクトを「直接の友だち」に限定し、それ以外のオブジェクトの内部構造に依存しないようにすることで、結合度(カップリング)の低減と変更容易性の向上を図ります。

  • 目的

    • モジュール間の依存を最小化し、影響範囲を局所化する
    • テストしやすいコード、リファクタリングしやすいコードを実現する
  • キーメッセージ

    • 「友だちだけに話す」
      • 自分 → 友だち(直接参照可能なオブジェクト)
      • 友だち → その先のオブジェクト、という2段階以上の呼び出しは避ける

2. 原則の核:最小限の知識(Principle of Least Knowledge)

「最小限の知識の原則」とは、オブジェクト指向設計において、各オブジェクトが知るべき他のオブジェクトを必要最小限に絞ることで、不必要な結合度を避ける考え方です。

主なポイント

  • 直接の友だちだけを参照
    • 自分が保持しているフィールド(プロパティ)
    • メソッドの引数として渡されたオブジェクト
    • 自身が生成したオブジェクト
  • 「友だちの友だち」には触らない
    • a.getB().getC() のように連鎖して呼び出すことは避ける
  • 依存を局所化
    • 変更があった際の影響範囲を限定し、リファクタリングやテストを容易にする

なぜ重要か

  1. 低結合・高凝集
    • モジュール間の不必要な依存を減らし、それぞれの責任範囲を明確化
  2. 保守性の向上
    • 実装の変更が「直接参照している箇所」のみで完結するため、バグの波及リスクを低減
  3. テスタビリティの改善
    • モック対象を限定しやすく、ユニットテストが書きやすくなる

3. 日常例で理解する“友だちだけに話す”アナロジー

日常のシーンで「デメテルの法則」をイメージすると、オブジェクト指向の“友だちだけに話す”という考え方が直感的に理解できます。

例1: 料理のキッチン

  • シェフ(あなた)アシスタントシェフ に「野菜を切って」と直接指示
  • アシスタントシェフ → 包丁へは指示しない
  • ❌ 避けるべきパターン:
    • シェフ → アシスタントA → アシスタントB → 包丁

例2: 会社の業務指示

  • マネージャー直属の部下 にタスクを依頼
  • 部下 → その部下(部下の部下)には依頼しない
  • ❌ 避けるべきパターン:
    • マネージャー → 部下A → 部下B → 実務担当者

ポイント

  • 対象はあくまで“直接の友だち”
    • 中継(友だちの友だち)を介した指示は避ける
  • 指示系統を簡潔に
    • 無駄な経路を減らし、混乱や誤解を防止
  • 変更時の影響を局所化
    • 誰に何を指示しているか明確になることで、トラブル発生時の原因追跡が容易に

4. 【対比】違反例:深いメソッドチェーンが招く結合度の増大

深いメソッドチェーンは、オブジェクトの内部構造に過度に依存してしまう典型的なアンチパターンです。
内部構造が変わるたびに呼び出し元すべてを修正しなければならず、保守性が著しく低下します。

違反例

// Order → Customer → Address → ZipCode まで直接たどっている
String zip = order.getCustomer()
                  .getAddress()
                  .getZipCode();

問題点

  1. Order が Customer の内部クラス Address の構造(getAddress() → getZipCode())まで知っている
  2. Address の実装が変わると、この呼び出し箇所すべてを修正する必要がある
  3. テスト時にモックすべきオブジェクトが多く、ユニットテストが煩雑になる

5. 【対比】遵守例:中継メソッド/Facadeパターンによる情報隠蔽

深いメソッドチェーンを避け、必要な情報だけを提供する中継メソッドや Facade パターンを使うことで、呼び出し元は内部構造を意識せずに済みます。

5.1 中継メソッドの追加

// Order.java
public class Order {
    private Customer customer;

    // 中継メソッドを追加
    public String getCustomerZipCode() {
        // Customer が内部で Address を参照し、ZipCode を返す
        return customer.getZipCode();
    }
}

// Customer.java
public class Customer {
    private Address address;

    // おまけの中継メソッド
    public String getZipCode() {
        return address.getZipCode();
    }
}

// 呼び出し側
Order order = ;
String zip = order.getCustomerZipCode();

ポイント

  • Order は Customer のみとしか会話せず、Address の存在を知らない
  • Customer の実装変更は Customer クラス内で完結し、Order 側に影響を及ぼさない

5.2 Facade パターンによる情報隠蔽

// AddressFacade.java
public class AddressFacade {
    public static String resolveZipCode(Order order) {
        // チェーンをここだけに閉じ込める
        return order.getCustomer().getAddress().getZipCode();
    }
}

// 呼び出し側
Order order = ;
String zip = AddressFacade.resolveZipCode(order);

ポイント

  • 深いチェーン呼び出しを Facade に閉じ込め、呼び出し側は AddressFacade だけを参照
  • Facade 内の実装変更は呼び出し元に波及しない

5.3 メリットまとめ

  • 低結合:呼び出し元が内部モデルを知らずに済む
  • 可読性向上:意図がメソッド名から明確に読み取れる
  • 保守性向上:内部構造変更時の影響範囲が限定される
  • テスト容易:単純なモックポイントが少なく、ユニットテストが書きやすい

6. 応用:ドメイン駆動設計におけるバウンデッドコンテキスト間の境界

ドメイン駆動設計(DDD)では、大規模システムを「バウンデッドコンテキスト(Bounded Context)」と呼ばれる論理的なモジュールに分割し、それぞれが独立して振る舞います。
ここでもデメテルの法則を応用し、他コンテキストの内部構造を知らずにインターフェースを通じてのみ命令・問い合わせを行うことで、結合度を抑制します。

6.1 バウンデッドコンテキストの分離

  • 顧客管理コンテキスト(CustomerContext)
    • 顧客情報の永続化/検証
  • 注文管理コンテキスト(OrderContext)
    • 注文の生成/支払い
  • 配送管理コンテキスト(ShippingContext)
    • 配送ラベルの発行/配達状況追跡

各コンテキストは自分のモデルだけを直接扱い、他コンテキストのモデルには触れません。

6.2 インターフェース経由のやり取り

// CustomerContext が提供する公開サービス
public interface CustomerService {
    CustomerDto findCustomerById(UUID customerId);
}

// OrderContext のユースケース
public class OrderApplication {
    private final CustomerService customerService;
    private final OrderRepository orderRepo;

    public OrderApplication(CustomerService cs, OrderRepository or) {
        this.customerService = cs;
        this.orderRepo = or;
    }

    public void createOrder(UUID customerId, OrderData data) {
        // 顧客の内部構造は知らず、CustomerDto という外部契約だけを受け取る
        CustomerDto customer = customerService.findCustomerById(customerId);
        Order order = Order.from(customer, data);
        orderRepo.save(order);
    }
}
  • OrderApplication は CustomerDto 以外の Customer の内部構造(住所や支払い情報など)を一切知らない
  • 他コンテキストの変更は公開サービス側で吸収され、注文側のコードに影響しない

6.3 メッセージングやAPIゲートウェイによる境界強化

  • メッセージング(イベント駆動・Kafka/PubSub)
    • イベントペイロードだけを公開し、各サービスは購読したイベントのスキーマのみを理解
  • APIゲートウェイ
    • REST/GraphQL で外側に公開するインターフェースを一元管理し、内部APIの変更をカプセル化

まとめと次の一歩:保守性向上のメリットと適用上の注意点

デメテルの法則を適用することで以下のメリットが得られます。

  • 結合度の低減
    モジュール間の依存を限定し、変更時の影響範囲を局所化
  • 可読性・理解性の向上
    呼び出し元が必要な情報のみを扱うため、意図が明確に伝わる
  • テスタビリティの改善
    モック対象を限定でき、ユニットテストのセットアップが容易
  • リファクタリングの安全性
    内部実装の変更が呼び出し元に波及しにくく、安心して改修できる

適用上の注意点

  1. メソッドの肥大化に注意
    • 中継メソッドを増やしすぎるとクラスが肥大化する可能性がある
    • 必要以上にラッパーを作る前に、本当に必要かを検討
  2. バランス感覚の重要性
    • 小規模プロジェクトや短期開発では過度な適用がオーバーヘッドに
    • チームの開発スタイルやコードベースの規模に合わせて緩急をつける
0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?