5
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

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

デメテルの法則、Law of Demeterは、「最小知識の原則」とも呼ばれるオブジェクト指向の設計指針だ。

よく、次の言葉で説明される。

直接の友達とだけ話せ。見知らぬ人とは話すな。

ここでいう「友達」とは、自分自身や、自分が直接保持しているオブジェクト、引数として受け取ったオブジェクトなどを指す。

反対に、直接知っている相手を経由して、その内部にある別のオブジェクトまで次々と辿ると、相手の内部構造を深く知ることになる。

デメテルの法則が避けようとしているのは、この状態だ。

外側のコードが内部構造を知りすぎると、内部の持ち方を変えただけでも、利用側まで修正しなければならなくなる。

本来、外側のコードが知るべきなのは、内部でどう処理しているかではない。そのオブジェクトに何を依頼できるかだ。

つまり、デメテルの法則の本質は、. の数を減らすことではない。

内部の階層構造への依存を減らし、変更の影響を内側へ閉じ込めることにある。

2. レストランでたとえると

レストランでパスタを注文するとき、客はホールスタッフに伝える。

パスタをひとつお願いします。

ホールスタッフは注文を厨房へ伝える。料理人は倉庫や冷蔵庫から食材を取り出し、調理する。

このとき、客は店の内部構造を知らなくていい。

  • 厨房がどこにあるか
  • 冷蔵庫がどこにあるか
  • パスタの在庫を誰が管理しているか
  • どの料理人が調理するか

こうした情報は、レストランの内側の事情だ。

客が知る必要があるのは、「パスタを注文できるか」ということだけである。

コードで表すと、次のようになる。

final canOrder = restaurant.canOrder(MenuItem.pasta);
enum MenuItem {
  pasta,
  curry,
  steak,
}

class Restaurant {
  Restaurant(this._kitchen);

  final Kitchen _kitchen;

  bool canOrder(MenuItem item) {
    return _kitchen.canCook(item);
  }
}
class Kitchen {
  Kitchen(this._inventory);

  final Inventory _inventory;

  bool canCook(MenuItem item) {
    return _inventory.hasIngredientsFor(item);
  }
}
class Inventory {
  Inventory(this._stocks);

  final Map<MenuItem, int> _stocks;

  bool hasIngredientsFor(MenuItem item) {
    return (_stocks[item] ?? 0) > 0;
  }
}

客に相当する呼び出し側は、Restaurant に問い合わせるだけだ。

内部では、次の順番で処理されている。

Restaurant
└── Kitchen
    └── Inventory

しかし、呼び出し側からはその構造が見えない。

厨房の中に在庫を置くのをやめて、外部の在庫管理サービスを使うようになっても、客の注文方法は変わらない。

class Restaurant {
  Restaurant(this._inventoryService);

  final InventoryService _inventoryService;

  Future<bool> canOrder(MenuItem item) {
    return _inventoryService.isAvailable(item);
  }
}

呼び出し側は、引き続き同じように問い合わせられる。

final canOrder =
    await restaurant.canOrder(MenuItem.pasta);

厨房の構造が変わっても、客には影響しない。

一方、次のコードでは、客が厨房の奥まで自分で歩いている。

final pastaStock = restaurant
    .kitchen
    .storage
    .ingredients
    .pasta
    .remaining;

レストランでの行動に置き換えると、次のようになる。

  1. 客がホールを抜ける
  2. 厨房に入る
  3. 食材倉庫を探す
  4. パスタの棚を探す
  5. 在庫数を確認する

客がレストランの内部事情を知りすぎている。

デメテルの法則は、このような状態を避けるための指針だ。

客は厨房へ行かず、ホールスタッフに目的を伝える。

コードでも同じように、内部オブジェクトを辿るのではなく、直接知っているオブジェクトに目的を伝える。

final canOrder =
    restaurant.canOrder(MenuItem.pasta);

この考え方は、Tell, Don't Ask ともつながっている。

たとえば、次のコードを考える。

if (order.customer.wallet.balance >= order.totalPrice) {
  order.customer.wallet.balance -= order.totalPrice;
  order.status = OrderStatus.paid;
}

外側のコードが、注文から客を取り出し、客から財布を取り出し、残高を確認し、金額を引き、注文状態を書き換えている。

レストランで言えば、客がレジの裏へ入り、会計システムを直接操作している状態だ。

呼び出し側がやりたいことは、「注文を支払済みにする」ことだ。

ならば、目的をそのまま命令すればいい。

final result = order.pay();
class Order {
  Order({
    required this.totalPrice,
    required PaymentMethod paymentMethod,
  }) : _paymentMethod = paymentMethod;

  final Money totalPrice;
  final PaymentMethod _paymentMethod;

  OrderStatus _status = OrderStatus.pending;

  OrderStatus get status => _status;

  PaymentResult pay() {
    final result = _paymentMethod.charge(totalPrice);

    if (result case PaymentSuccess()) {
      _status = OrderStatus.paid;
    }

    return result;
  }
}

呼び出し側は、財布や残高、決済方法の構造を知らない。

注文に対して「支払え」と伝えるだけである。

状態を取り出して外側で処理するのではなく、処理を持つオブジェクトへ命令する。

これが Tell, Don't Ask の考え方だ。

3. デメテルの法則を守らないとどうなるのか

デメテルの法則を守らないコードでは、内部構造の変更が外側へ漏れやすくなる。

主に、次の3つの問題が起きる。

3.1 変更の影響範囲が広がる

最も大きな問題は、内部構造を変更したときに、無関係な呼び出し側まで壊れることだ。

たとえば、最初は次の構造だったとする。

Restaurant
└── Kitchen
    └── Storage
        └── Ingredients

呼び出し側は、パスタの在庫を次のように取得している。

final pastaStock = restaurant
    .kitchen
    .storage
    .ingredients
    .pasta
    .remaining;

後から、在庫管理を独立した Inventory クラスへ移した。

Restaurant
├── Kitchen
└── Inventory

すると、すべての呼び出し側を書き換える必要がある。

final pastaStock = restaurant
    .inventory
    .pasta
    .remaining;

さらに、外部サービスへ移行したら、また変更になる。

final pastaStock = await restaurant
    .inventoryClient
    .fetchRemaining(MenuItem.pasta);

客が知りたいことは変わっていない。

知りたいのは、パスタを注文できるかどうかだ。

それにもかかわらず、レストラン内部の実装変更によって、客側のコードが何度も壊れている。

これは密結合の状態である。

一方、最初から次のインターフェースを用意しておけば、呼び出し側は変わらない。

final canOrder =
    await restaurant.canOrder(MenuItem.pasta);

内部でどのように在庫を管理するかは、Restaurant の実装詳細として隠せる。

3.2 呼び出し側が業務ルールを持ち始める

内部構造へ自由にアクセスできると、呼び出し側がデータを取り出し、そこで業務ルールを実装し始める。

if (restaurant
        .kitchen
        .inventory
        .pasta
        .remaining >
    0) {
  restaurant
      .kitchen
      .inventory
      .pasta
      .remaining--;

  order.status = OrderStatus.accepted;
}

このコードでは、呼び出し側が次のルールを知っている。

  • 在庫が1以上なら注文を受け付ける
  • 注文を受け付けたら在庫を1減らす
  • その後、注文状態を accepted にする

本来、これは注文受付や厨房の責務である。

外側へロジックが漏れると、同じ業務ルールが複数箇所に分散する。

// 注文画面
if (remaining > 0) {
  remaining--;
}

// 管理画面
if (remaining >= quantity) {
  remaining -= quantity;
}

// バッチ処理
if (remaining != 0) {
  remaining = remaining - 1;
}

似たような処理が増えると、ルール変更時に修正漏れが起きる。

注文受付の条件を一箇所へ閉じれば、その問題を避けられる。

class Restaurant {
  Restaurant(this._kitchen);

  final Kitchen _kitchen;

  OrderResult acceptOrder(MenuItem item) {
    if (!_kitchen.canCook(item)) {
      return const OrderRejected.outOfStock();
    }

    _kitchen.prepare(item);

    return const OrderAccepted();
  }
}

呼び出し側は、在庫数や減算方法を知らない。

final result =
    restaurant.acceptOrder(MenuItem.pasta);

3.3 テストが内部構造に引きずられる

内部構造へ依存しているコードは、テストでも大量のオブジェクトを組み立てる必要がある。

final restaurant = Restaurant(
  kitchen: Kitchen(
    storage: Storage(
      ingredients: Ingredients(
        pasta: PastaStock(
          remaining: 10,
        ),
      ),
    ),
  ),
);

テストしたいのは、「パスタの注文を受け付けられるか」だけなのに、厨房、倉庫、食材一覧、パスタ在庫まで準備している。

内部構造が変わるたびに、テストのセットアップも壊れる。

一方、公開された振る舞いに依存すれば、テスト対象を絞りやすい。

final kitchen = FakeKitchen(
  availableItems: {
    MenuItem.pasta,
  },
);

final restaurant = Restaurant(kitchen);

expect(
  restaurant.canOrder(MenuItem.pasta),
  isTrue,
);

テストが内部構造ではなく、公開された振る舞いを確認する形になる。

これはテストの可読性だけでなく、リファクタリング耐性にもつながる。

4. 注意点

デメテルの法則は、機械的に守ればよいルールではない。

. の数だけで判断しない

次のような Fluent interface は、. が続いていても問題とは限らない。

final order = orderBuilder
    .add(MenuItem.pasta)
    .large()
    .withDrink()
    .build();

これは、客が厨房の奥へ進んでいるのではなく、同じホールスタッフに注文を続けている状態だ。

一方、次のコードは内部構造を辿っている。

final stock = restaurant
    .kitchen
    .storage
    .pasta
    .remaining;

見るべきなのは . の数ではない。

公開された操作を続けているのか、内部のオブジェクト構造を辿っているのかで判断する。

DTOやJSONは分けて考える

JSONやDTOは、レストランで言えば客に渡されたメニュー表に近い。

final price = menuJson['items'][0]['price'];

このコードには型安全性や null 安全性の問題はあるが、振る舞いを持つドメインオブジェクトの内部へ侵入しているとは限らない。

公開されるためのデータまで、すべてメソッドの後ろへ隠す必要はない。

委譲メソッドを増やしすぎない

客を厨房へ入れないために、何でも Restaurant 経由にすると、今度は Restaurant が何でも屋になる。

restaurant.canOrder(MenuItem.pasta);
restaurant.currentChefName;
restaurant.refrigeratorTemperature;
restaurant.supplierAddress;

canOrder はレストランとして自然な振る舞いだが、冷蔵庫の温度や仕入れ先の住所まで扱い始めると、責務が広がりすぎる。

判断基準は、そのメソッドが単なる内部データの横流しではなく、呼び出し側の目的を表しているかどうかだ。

デメテルの法則を適用するときは、次のトレードオフを見る必要がある。

内部構造への依存を減らす利益と、委譲メソッドによる責務肥大化のコスト

5. まとめ

デメテルの法則の本質は、. の数を減らすことではない。

外側のコードが、オブジェクトの内部構造を知りすぎないようにすることだ。

レストランで言えば、客が厨房や倉庫まで歩いていくのではなく、ホールスタッフに目的を伝える。

// 内部構造を辿る
final stock =
    restaurant.kitchen.storage.pasta.remaining;

// 目的を伝える
final canOrder =
    restaurant.canOrder(MenuItem.pasta);

ただし、何でもホールスタッフ経由にすると、今度は窓口となるクラスが肥大化する。

重要なのは、法則を機械的に守ることではない。

客を厨房に入れず、同時にホールスタッフを何でも屋にしない境界を選ぶことだ。

5
4
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
5
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?