1
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?

この記事は 株式会社カオナビ Advent Calendar 2025 の19日目(シーズン3)の記事です。

はじめに

はいさい!カオナビで労務・勤怠グループに所属している しまぶ だよ。

今回は、デメテルの法則について話してみたいと思います。

デメテルの法則は「直接の隣人とだけ話す」という設計原則です。

この法則に従うことで、オブジェクト同士の結合を弱くして、変更に強いコードが書けるようになります。

具体的には、メソッドが他人の内部構造(=実装詳細)を知ってしまう「知識の漏洩」を防ぐことができます。内部構造を知ってしまうと、その構造が変わったときに修正箇所が増えてしまうんですね。一箇所修正したらあっちこっち直さなければいけなくなったこと、あると思います。

今回はGoのコード例を使って、この法則について見ていきたいと思います。
それではレッツゴー。

デメテルの法則とは

デメテルの法則(Law of Demeter)について整理します。

「直接の隣人とだけ話して、見知らぬ人とは話すな」

これがデメテルの法則の基本です。

デメテルの法則 - Wikipedia
こちらでは「直接の友達とだけ話すこと」となっていますね。

もう少し具体的に言うと、メソッドが話しかけていい相手はこの範囲に留めよう、という指針です。

  • 自分自身
  • 引数として渡されたもの
  • 自分が生成したもの
  • 自分が直接保持しているもの(フィールド)

かんたんな例では、order.Customer.Address.City のような メッセージの連鎖(message chain) を避けよう、という話になります。
俗に「列車事故(train wrecks)」とも呼ばれます(Goならドット.、PHPなら 矢印-> がずらっと並ぶやつ)。

ただし重要なのは 「ドットの数」ではありません

  • a.b.c が常に悪いわけではない
  • 問題は、他人の内部構造を知ってしまうこと

デメテルの法則の本質は「内部構造の知識を漏らさない」ことです。

デメテルの法則が解決する問題

デメテルの法則が防ごうとしているのは「知識の漏洩」です。

たとえば order.Customer.Address.City というコードは、このような知識を外部に晒しています。

  • OrderCustomer を持つ
  • CustomerAddress を持つ
  • AddressCity を持つ

この知識が漏れると、構造が変わったときに修正箇所が増えます。

デメテルの法則に従うと、この知識を持つ場所を限定できます。

「何を」じゃなくて「どうやって」を知ってしまう問題

遠くにある振る舞いを実行するために、いくつものオブジェクトを横断していくコードは、こう言っているのと同じです。

「あそこに、いま欲しい振る舞いがある。それをどうやって手に入れたらいいか知っている。」

つまり、コードは自分が「何を」求めているのかだけじゃなくて、その求める振る舞いを得るために「どうやって」中間オブジェクトを通っていけばよいかまで知ってしまっています。

たとえば order.Customer.Address.City は、「配送先の都市名が欲しい」という「何を」だけじゃなくて、「Order → Customer → Address → City という経路で取得する」という「どうやって」まで知っている状態です。

この「どうやって」の知識が問題です。経路が変わったら、その知識を持っているすべての場所を直さなければいけなくなります。

デメテルの法則は、この「どうやって」を知らなくて済むようにするための法則とも言えます。

会社組織で例えるなら

経理部のしまぶさんが配送先を確認したいとき、こんな状態になっていると困ります。

悪い例

経理部のしまぶさん
→ 注文管理部の注文データを直接見て
→ 顧客管理部の顧客データを直接見て
→ 顧客の住所データを直接見て
→ 住所の都市名を取得

しまぶさんが顧客管理部の"内部データ構造"まで知ってしまっている状態です。
知っているがゆえに、顧客管理部がデータの持ち方を変えると、経理部まで修正に巻き込まれてしまいます。

良い例

経理部のしまぶさん
↓
注文管理部の佐藤さんに「配送先を教えて」と聞く
↓
佐藤さんが顧客管理部の田中さんに聞く
↓
田中さんが住所データから確認して返す

この形なら、顧客管理部の内部が変わっても、経理部のしまぶさんは「配送先を教えて」で済みます。

実際のコード(あまり良くない例)

type Address struct {
	PostalCode string
	City       string
	Street     string
}

type Customer struct {
	Name    string
	Address *Address
}

type Order struct {
	ID       string
	Customer *Customer
	Amount   int
}

// 悪い例。深いアクセスチェーン(nilにも弱い)
func PrintOrderDetails(order *Order) {
	fmt.Printf("注文ID: %s\n", order.ID)
	fmt.Printf("顧客名: %s\n", order.Customer.Name)
	fmt.Printf("配送先: %s %s %s\n",
		order.Customer.Address.PostalCode,
		order.Customer.Address.City,
		order.Customer.Address.Street)
}
実行可能なコードはこちら
package main

import "fmt"

type Address struct {
	PostalCode string
	City       string
	Street     string
}

type Customer struct {
	Name    string
	Address *Address
}

type Order struct {
	ID       string
	Customer *Customer
	Amount   int
}

// 悪い例。深いアクセスチェーン(nilにも弱い)
func PrintOrderDetails(order *Order) {
	fmt.Printf("注文ID: %s\n", order.ID)
	fmt.Printf("顧客名: %s\n", order.Customer.Name)
	fmt.Printf("配送先: %s %s %s\n",
		order.Customer.Address.PostalCode,
		order.Customer.Address.City,
		order.Customer.Address.Street)
}

func main() {
	address := &Address{
		PostalCode: "150-0001",
		City:       "東京都渋谷区",
		Street:     "神宮前1-2-3",
	}

	customer := &Customer{
		Name:    "田中太郎",
		Address: address,
	}

	order := &Order{
		ID:       "ORD-001",
		Customer: customer,
		Amount:   5000,
	}

	// 注文詳細を表示
	PrintOrderDetails(order)
}

実行結果:

注文ID: ORD-001
顧客名: 田中太郎
配送先: 150-0001 東京都渋谷区 神宮前1-2-3

https://go.dev/play/p/y86ZRm8XZfN

問題は、デメテルの法則に違反していることです。

  • 他人の内部構造を知っている
    • PrintOrderDetailsCustomer / Address の構造を知ってしまう
  • 知識が連鎖している
    • Order → Customer → Address という構造を前提にしている
  • 変更が波及する
    • 配送先の持ち方が変わると呼び出し側も修正が必要
  • nilに弱い
    • 構造の途中で nil があると panic
    • 存在を意識しないといけない

デメテルの法則では「直接の隣人(この場合は Order)とだけ話す」べきなのに、CustomerAddress という「見知らぬ人」と話してしまっています。

デメテルの法則に従ってみる

デメテルの法則に従うには、PrintOrderDetailsOrder だけと話すようにします。

ポイントは2つです。

  • Order に配送先情報を問い合わせる
  • 内部構造(CustomerAddress)を直接触らない
type Address struct {
	postalCode string
	city       string
	street     string
}

type DeliveryAddress struct {
	City, Street, PostalCode string
}

func (d DeliveryAddress) String() string {
    // 取得可否は ok で判定する(ここではゼロ値を空表示)
	if d.City == "" && d.Street == "" && d.PostalCode == "" {
		return ""
	}
	return fmt.Sprintf("%s %s %s", d.PostalCode, d.City, d.Street)
}

type Customer struct {
	name    string
	address *Address
}

func (c Customer) Name() string {
	return c.name
}

// DeliveryAddress は「配送先」という意味のあるまとまりを返す
func (c Customer) DeliveryAddress() (DeliveryAddress, bool) {
	if c.address == nil {
		return DeliveryAddress{}, false
	}

	return DeliveryAddress{
		City:       c.address.city,
		Street:     c.address.street,
		PostalCode: c.address.postalCode,
	}, true
}

type Order struct {
	id       string
	customer *Customer
	amount   int
}

func (o Order) ID() string {
	return o.id
}

func (o Order) CustomerName() (string, bool) {
	if o.customer == nil {
		return "", false
	}
	return o.customer.Name(), true
}

func (o Order) DeliveryAddress() (DeliveryAddress, bool) {
	if o.customer == nil {
		return DeliveryAddress{}, false
	}
	return o.customer.DeliveryAddress()
}

// PrintOrderDetails はOrderだけに話しかける
func PrintOrderDetails(o Order) {
	fmt.Printf("注文ID: %s\n", o.ID())

	if name, ok := o.CustomerName(); ok {
		fmt.Printf("顧客名: %s\n", name)
	} else {
		fmt.Printf("顧客名: (不明)\n")
	}

	if addr, ok := o.DeliveryAddress(); ok {
		fmt.Printf("配送先: %s\n", addr.String())
	} else {
		fmt.Printf("配送先: (不明)\n")
	}
}
実行可能なコードはこちら
package main

import (
	"fmt"
)

type Address struct {
	postalCode string
	city       string
	street     string
}

type DeliveryAddress struct {
	City, Street, PostalCode string
}

func (d DeliveryAddress) String() string {
    // 取得可否は ok で判定する(ここではゼロ値を空表示)
	if d.City == "" && d.Street == "" && d.PostalCode == "" {
		return ""
	}
	return fmt.Sprintf("%s %s %s", d.PostalCode, d.City, d.Street)
}

type Customer struct {
	name    string
	address *Address
}

func (c Customer) Name() string {
	return c.name
}

// DeliveryAddress は「配送先」という意味のあるまとまりを返す
func (c Customer) DeliveryAddress() (DeliveryAddress, bool) {
	if c.address == nil {
		return DeliveryAddress{}, false
	}

	return DeliveryAddress{
		City:       c.address.city,
		Street:     c.address.street,
		PostalCode: c.address.postalCode,
	}, true
}

type Order struct {
	id       string
	customer *Customer
	amount   int
}

func (o Order) ID() string {
	return o.id
}

func (o Order) CustomerName() (string, bool) {
	if o.customer == nil {
		return "", false
	}
	return o.customer.Name(), true
}

func (o Order) DeliveryAddress() (DeliveryAddress, bool) {
	if o.customer == nil {
		return DeliveryAddress{}, false
	}
	return o.customer.DeliveryAddress()
}

// PrintOrderDetails はOrderだけに話しかける
func PrintOrderDetails(o Order) {
	fmt.Printf("注文ID: %s\n", o.ID())

	if name, ok := o.CustomerName(); ok {
		fmt.Printf("顧客名: %s\n", name)
	} else {
		fmt.Printf("顧客名: (不明)\n")
	}

	if addr, ok := o.DeliveryAddress(); ok {
		fmt.Printf("配送先: %s\n", addr.String())
	} else {
		fmt.Printf("配送先: (不明)\n")
	}
}

func main() {
	address := &Address{
		postalCode: "150-0001",
		city:       "東京都渋谷区",
		street:     "神宮前1-2-3",
	}
	customer := &Customer{name: "田中太郎", address: address}
	order := Order{id: "ORD-001", customer: customer, amount: 5000}

	PrintOrderDetails(order)
}

実行結果:

注文ID: ORD-001
顧客名: 田中太郎
配送先: 150-0001 東京都渋谷区 神宮前1-2-3

https://go.dev/play/p/hsZFEUDjs-Q

デメテルの法則に従うとどうなるか

  • 話すのは直接の隣人とだけ
    • PrintOrderDetailsOrder だけを知っている
  • 内部構造を隠せる
    • CustomerAddress の構造変更が外に影響しない
  • 知識が限定される
    • 配送先の持ち方を知る必要がない

これがデメテルの法則の「直接の隣人とだけ話す」という原則です。

PrintOrderDetailsOrder だけに問い合わせて、その奥にある CustomerAddress には直接触れていません。

ただし、この例は「注文詳細を表示する」という用途に絞った窓口を作っています。フィールド単位の転送メソッドが増えてきたら、それは伝言係の兆候かもしれません。

デメテルの法則のやりすぎに注意

デメテルの法則を盲目的に適用すると別の問題が出ます。

伝言メソッドが増える

デメテルの法則を厳密に守ろうとすると、こうなりがちです。

func (o Order) CustomerAddressCity() string {
	if o.customer == nil {
		return ""
	}

	addr, ok := o.customer.DeliveryAddress()
	if !ok {
		return ""
	}

	return addr.City // ここまで欲しくなったりする
}

func (o Order) CustomerAddressStreet() string {
	if o.customer == nil {
		return ""
	}

	addr, ok := o.customer.DeliveryAddress()
	if !ok {
		return ""
	}

	return addr.Street // ここまで欲しくなったりする
}

// ...

これは単なる伝言係になってしまっています。

デメテルの法則に従っているけど、メソッドが増えすぎて保守性が下がります。

バランスの取り方

対策の一つは、意味のあるまとまりで返すことです。

先ほどの例で DeliveryAddress を使ったのがまさにこれです。City()Street() をバラバラに返すのではなく、「配送先」という意味のあるまとまりで返しています。

// フィールド単体を返す伝言係(あまり良くない)
func (o Order) CustomerAddressCity() string { ... }
func (o Order) CustomerAddressStreet() string { ... }
func (o Order) CustomerAddressPostalCode() string { ... }

// 意味のあるまとまりで返す(良い)
func (o Order) DeliveryAddress() (DeliveryAddress, bool) { ... }

デメテルの法則を適用しなくていい場面

  • Point{X,Y} のような小さな構造体は p.X で十分
  • 内部構造の知識が漏れても問題ない場面では無理に隠さない

デメテルの法則は「内部構造の知識漏洩を防ぐ」ための法則です。知識が漏れても問題ない場面では、無理に適用する必要はありません。

まとめ

デメテルの法則は 「直接の隣人とだけ話す」 という原則です。

デメテルの法則のポイント

  • 「何を」だけ知って「どうやって」は知らない
    • 欲しい振る舞いを得るための経路(中間オブジェクトの横断)を知らなくて済むようにする
  • 他人の内部構造(=実装詳細)を知らないようにする
    • メッセージの連鎖(a.b.c.d)を避ける
  • 知識の漏洩を防ぐ
    • 呼び出し側が知るべきことを減らす
  • 変更に強くなる
    • 内部構造の変更が外に波及しない

実践ポイント

  • 「ドット数」より「知識漏洩」を見る
  • 意味のあるまとまりで返す(DeliveryAddress など)
  • やりすぎて伝言係にならないよう注意

問題は「ドットの数」ではなく、呼び出し側が 他人の内部構造(=実装詳細) を前提にしてしまうことです。

「この呼び出し、他人の内部構造を前提にしてない?」という視点を持つと、デメテルの法則違反を見つけやすくなるかもしれません。

要するに、「相手の中身を前提にせず、窓口に頼る距離感で設計しよう」 という話でした。

何かの参考になるとうれしいです。

参考資料

こちらの記事は、以下書籍の内容を参考にしています。

オブジェクト指向設計実践ガイド | 技術評論社

デメテルの法則をはじめ、オブジェクト指向設計の原則を実践的に学べる名著です。コードはRubyで書かれていますが、原則は他の言語にも適用できます。より深く学びたい方にはぜひおすすめします。

1
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
1
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?