Featured image of post コードバーテンダー【Law of Demeter】隣の客の注文を覗かない〜バーテンダーの矜持〜

コードバーテンダー【Law of Demeter】隣の客の注文を覗かない〜バーテンダーの矜持〜

Law of Demeter(デメテルの法則)とは何か? Perl+Mooのコード例で、メソッドチェーンによる過剰な結合の問題と、handlesによる委譲での解決策を物語形式で解説します。

八杯目の夜。

扉を押すと、いつもと違う香りが出迎えた。

潮気を含んだ、凛とした空気。煙くはない。先週のラスティネイルの薬草の甘さでもない。海風のようで、けれどどこか温かい。

カウンターの向こうで、マスターが見慣れないボトルを手にしていた。グラスはもう用意されている。

いつもの席に座った。何も言わない。マスターも何も聞かない。八回目だ。もうそういう間柄になっている。

マスターがグラスにウイスキーを注いだ。琥珀色。先週のラスティネイルは二つの酒を重ねたカクテルだったけれど、今夜は一本のボトルだけ。

「……今夜はこれですか」

おまかせで、とは言わなかった。自分で選ぼうともしなかった。ただ——何を出されたのか、知りたかった。

「余市(よいち)。シングルカスクでございます」

「シングルカスク?」

「一つの樽だけで完結した原酒です。他の樽とブレンドしていません」

一口。力強いが透明感がある。潮の香りの奥に、蜂蜜のような丸い甘さ。先週のラスティネイルは「二つを重ねた」複雑さだったけれど、これは一つの樽の中にすべてがある。

「シンプルなのに、奥がある……」

「必要なものだけで完結しているからでしょう」

来店——伝言ゲームのような矢印

視線がカウンターの上を滑っていった。取り置きボトル。

もう「カウンターの端」とは呼べない。数えてみた。私の席から三つ目の位置。手を伸ばせば——もしかしたら、届くかもしれない距離。

麻布は前回からずれたまま。琥珀色が覗いている。七回目までは「近づいている気がする」と思っていた。でも今夜は、「気がする」じゃない。確実に近い。

口には出さなかった。ただ、視線を戻した。

扉がゆっくりと開いた。

ネクタイを緩めながら入ってくる男性。スーツの上着は腕にかけている。革靴。PCバッグ。残業帰りの空気。

前回の手順書さんとは対照的だった。あの人は扉を勢いよく開けて、座る前にPCを取り出していた。この人は静かに入ってきて、カウンターの両隣を確認してから一つ席を空けて座った。

ネクタイさん、と心の中で呼んだ。前のお客さんたちはTシャツだったりパーカーだったりしたけれど、この人はスーツにネクタイ。ネクタイを緩める仕草が、長い一日の終わりを物語っている。

「いらっしゃいませ。今夜は何をお召し上がりになりますか」

ネクタイさんはメニューに目を通してから答えた。「ハイボールをお願いします。……あの、ここはコードの相談もできると伺ったんですが」

落ち着いた声だった。急いでいない。困っているというよりも、何かを確認したいという温度。先週の手順書さんの切迫感とは、全く違う空気。

ハイボールを一口飲んで、ネクタイさんがPCを開いた。画面をマスターに向ける。

「弊社の注文管理システムなんですが——今度、顧客データモデルを再設計することになりまして」

「個人顧客と法人顧客を分ける改修です。影響範囲を調べたら——」

ネクタイさんが画面をこちらにも見えるように少し傾けた。コードが映っている。

「こういう書き方が、コードベース全体に散らばっていること がわかったんです」

画面に映っているのは——矢印。-> という記号が横に並んでいる。四つ、五つ。コードの中身はわからないけれど、「矢印がたくさんある」ことはわかる。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
package OrderReport;
use v5.36;
use Moo;
use Types::Standard qw(InstanceOf);

has order => (
    is  => 'ro',
    isa => InstanceOf['Order'],
);

sub shipping_label ($self) {
    my $name = $self->order->customer->name;
    my $city = $self->order->customer->address->city;
    my $zip  = $self->order->customer->address->zip_code;
    return "$name\n$zip $city";
}

sub total_with_tax ($self) {
    my $rate = $self->order->customer->tax_info->rate;
    return $self->order->amount * (1 + $rate);
}

Customer の構造を変えると、関係ないはずの場所まで芋づる式に壊れることがわかって。上司に報告したら『全部直せばいいじゃない』と言われたんですが——その『全部』がどれだけの規模になるか」

ネクタイさんの声には怒りも焦りもなかった。ただ、静かな疑問。「これでいいのか」という確認。

矢印が何本も並ぶコードを見て、ふと思った。

「伝言ゲームみたい」

ネクタイさんが振り返った。「……伝言ゲーム?」

「注文から顧客を辿って、顧客から住所を辿って、住所から都市を辿って——何段階も経由しないとデータに届かないんでしょう?」

「ええ、まさにそうです」

「うちのシステムも似たようなものかも」

口をついて出た。前にエンジニアに画面を見せてもらったときの記憶。「注文から顧客、顧客から住所って何段階も辿るんですよ」と説明されて、「ふーん」で済ませたあの日。

「矢印がずらっと並んでたわ。うちのエンジニアは当たり前だって言ってたけど」

マスターの視線が一瞬だけ、カウンターの下のほうに落ちた。何かを確認したように。それからすぐにネクタイさんに向き直った。

テイスティング——覗かないという作法

マスターがネクタイさんの画面をしばらく無言で見つめた。それから口を開いた。

「この shipping_label メソッドは、配送ラベルを作るためのものですね」

「はい」

「しかしここには——注文の中の、顧客の中の、住所の中の都市名。四つの層の奥にある情報を、一番外側から直接覗きに行っています」

「必要な情報を辿れば取れますので。不便はなかったんですが……」

「伝言ゲームっていうより、覗き見に近くない?」

思わず口を挟んでいた。自分でも驚いたけれど、言ってしまったものは仕方ない。

「Aさんが知りたいことを、BさんやCさんを経由して聞くんじゃなくて——直接、Bさんの引き出しを開けて中身を見てる感じがする」

ネクタイさんが少し考えて頷いた。「近いです。OrderReport が Customer の中の Address の中身まで直接知っている。Customer に許可を取らずに、中の構造を全部前提にしたコードです」

「覗き見……」

その言葉を反芻していたら、以前聞いた名前がふと浮かんだ。

「これって——もしかして、Feature Envy? 他のクラスのデータを羨んでるってやつ」

マスターが穏やかに首を振った。「似ていますが、少し違います」

間違えた。でも——名前を出せたこと自体に、少しだけ驚いている自分がいた。Feature Envy なんて言葉、半年前の私は知りもしなかった。

「Feature Envy は、自分のものではないデータを使いたがること。今夜の問題は——知るべきでない相手の構造に、立ち入ってしまっていることです」

ネクタイさんが静かに言った。「……Law of Demeter。デメテルの法則ですね」

マスターが小さく頷いた。

「デメテル? ギリシャ神話の?」

「ええ。収穫の女神の名を冠したプロジェクトで生まれた法則です」

マスターがカウンターの上にコースターを一枚置いた。ネクタイさんの空になったハイボールのグラスを下げながら。

直接の友人とだけ話せ。見知らぬ者とは話すな——そういう法則です」

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

何かが頭の中で動いた。前回の「手順ではなく構造で」。その前の「間に立つだけの存在は要らない」。点が少しずつ線になりつつある。でも、まだ全体は見えない。

「お客さま」。マスターがネクタイさんに向き直った。「この OrderReport が壊れるのは、どのような変更が起きたときでしょう」

ネクタイさんが画面を見直した。指がコードの行をなぞる。

「たとえば Customer が address メソッドを primary_address に変えたら——OrderReport が壊れます。Customer の内部構造を直接知っているから」

「Order のことも、Address のことも——本来、OrderReport が知る必要のない相手の事情です」

マスターが余市のボトルをカウンターに戻しながら、続けた。

「私はカウンターの中にいます。お客さまが何を召し上がりたいかは、直接お聞きします」

一拍の間。

「しかし——隣のお客さまがどなたと来店されたか、その方が何を頼まれたか、そのグラスの中身は何であるか。それを覗くことは、いたしません」

ネクタイさんが顔を上げた。「直接の相手以外の内部に触れるな、ということですか」

「ええ。知る必要のないことを知ってしまえば——それが変わったとき、すべてが壊れます」

ブレンド——知らないことは、壊れない

「では」。マスターがネクタイさんに問いかけた。「この OrderReport は、Order に何を聞けばよいのでしょう」

ネクタイさんが考え込んだ。指が画面の上で止まっている。先週の手順書さんはキーボードを早打ちしていたけれど、この人の手は静かだ。

「……配送先の住所がほしいだけです。Customer から辿る必要は、本来ない」

「そうです。OrderReport は Order に、配送先をくださいと伝えればいい。Order がどうやって Customer から住所を引き出すかは——Order の仕事です」

ネクタイさんが呟いた。「Tell, Don’t Ask……聞くのではなく、伝える」

それから不意に、苦笑した。

「皮肉ですよね」

「と言いますと?」

「弊社は組織の階層を厳密に守る文化です。部長が担当者のデスクに直接行くことは絶対にない。必ず課長を通す」

ネクタイさんが画面に目を落とした。「——なのに、コードではそのルールを完全に無視していました」

「あ、それわかる」

思わず声が出た。うちは大企業じゃないけど、報告ラインを飛ばされると困るのは同じだ。

「うちも小さい会社だけど、部下が私を飛ばして取引先に直接連絡したら——困るもの。なんで、コードだとそれがまかり通るんだろう」

ネクタイさんが静かに答えた。「慣習ですかね。動いてるから、誰も疑問に思わなかった」

——動いてるから。

その言葉が耳に引っかかった。自分も何度も使った言葉だ。でも今は、その言葉の軽さが少しだけわかる。

「Moo であれば——handles が使えます」

マスターがネクタイさんのPCに視線を送った。ネクタイさんがキーを叩き、新しいファイルを開く。マスターが横から指示する内容を、ネクタイさんが書き換えていく。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
package Address;
use v5.36;
use Moo;
use Types::Standard qw(Str);

has city     => (is => 'ro', isa => Str);
has zip_code => (is => 'ro', isa => Str);

sub format ($self) {
    return $self->zip_code . ' ' . $self->city;
}

package Customer;
use v5.36;
use Moo;
use Types::Standard qw(InstanceOf Str);

has name => (is => 'ro', isa => Str);

has address => (
    is      => 'ro',
    isa     => InstanceOf['Address'],
    handles => { formatted_address => 'format' },
);

has tax_info => (
    is      => 'ro',
    isa     => InstanceOf['TaxInfo'],
    handles => { tax_rate => 'rate' },
);

package Order;
use v5.36;
use Moo;
use Types::Standard qw(InstanceOf Num);

has customer => (
    is      => 'ro',
    isa     => InstanceOf['Customer'],
    handles => {
        customer_name    => 'name',
        shipping_address => 'formatted_address',
        tax_rate         => 'tax_rate',
    },
);

has amount => (is => 'ro', isa => Num);

package OrderReport;
use v5.36;
use Moo;
use Types::Standard qw(InstanceOf);

has order => (
    is      => 'ro',
    isa     => InstanceOf['Order'],
    handles => [qw(customer_name shipping_address tax_rate amount)],
);

sub shipping_label ($self) {
    return $self->customer_name . "\n" . $self->shipping_address;
}

sub total_with_tax ($self) {
    return $self->amount * (1 + $self->tax_rate);
}

「各クラスが、直接の隣人にだけ委譲しています」

マスターが画面を指しながら説明した。

「Customer が Address の formatformatted_address として公開し、Order がそれを shipping_address として引き受ける。OrderReport は Order だけを知っていればいい」

ネクタイさんが画面を見つめている。指がコードをゆっくりなぞった。

「Customer の中に Address があることを、OrderReport は知らない……。Order の中に Customer があることすら」

「ええ。知る必要がありません」

ここで、私の中に引っかかるものがあった。

「あれ……でも。前に来たお客さんの話を思い出したんだけど」

マスターとネクタイさんが私を見た。

「何もしないで通すだけのクラスがダメだって言ってたわよね。Middle Man って。これも——通してるだけじゃないの?」

ネクタイさんが首を傾げて、それから答えた。

「Middle Man は、何の仕事も持たずにただ転送するだけのクラスです。今回の Order は注文金額を持っていて、Customer との関係も管理していて——自分の存在理由がある。handles は、その上で問い合わせにも答えられるようにしているだけで」

「……空っぽの中間管理職と、自分の仕事をしつつ窓口も兼ねる人の違い?」

ネクタイさんが目を見開いた。「まさにそうです。弊社でいえば、管理部門が何も判断せずメールを転送するだけなのが Middle Man。営業部が自分の仕事をしながら顧客窓口も担うのが handles です」

なるほど。前回聞いた話と、今夜の話が繋がった。間に立つこと自体が悪いんじゃない。間に立って何もしないのが問題だった。

「でも」。もう一つ、気になることがあった。「結局は同じデータに辿り着くんでしょう? 部長が直接行っても、課長を経由しても、結果は同じじゃない?」

ネクタイさんが少し考えてから答えた。

「結果は同じです。でも——たとえば担当者が異動したとき。部長が直接デスクに行く仕組みだと、部長のルーティンが壊れます。課長を経由していれば、課長が新しい担当者を見つけて調整するだけで済む」

マスターが頷いた。「影響範囲が局所化されます。Address の構造が変わっても、修正するのは Customer の handles だけ。Order や OrderReport は何も変わりません」

「ああ……さっきは Customer を変えたら全部壊れるって言ってたけど、handles があれば Customer の中だけで吸収できるってこと」

「ええ」

マスターがグラスを拭いた。穏やかだけれど、はっきりとした声で。

知らないことは、壊れません

ラストオーダー——まだ、早いですよ

ネクタイさんがPCを閉じた。落ち着いた動作で。何かのメモを保存してから画面を閉じたのが見えた。

「弊社のコードベースで、矢印の連鎖が三段以上あるものを検索してみます」

少し間を置いて。

「何箇所見つかるか、正直少し怖いですが」

立ち上がった。スーツの上着を羽織り、ネクタイを締め直している。来たときに緩めたのを、戻していた。何かのスイッチが切り替わったように見えた。

マスターに一礼した。「ありがとうございました。明日から、少しずつ直していきます」

それから私にも軽く会釈した。「伝言ゲームの喩え、わかりやすかったです。社内の説明で使わせてもらいます」

扉が閉まった。来たときと同じくらい静かに。

バーに沈黙が戻った。

余市のグラスに残った最後の一口を傾けた。潮気の余韻。一つの樽だけで完結した味。混ぜていないのに複雑で、でも余計なものは何もない。

「デメテルの法則、か」

覚えたての名前を口の中で転がした。直接の友人とだけ話せ。見知らぬ者とは話すな。Feature Envyと間違えたのは恥ずかしかったけれど——名前を出せたこと自体が、半年前の自分には考えられないことだった。

「マスター」

「はい」

「一つの樽で完結するって——いいですね。他の樽のことを気にしなくていいって」

マスターがグラスを下げた。

「一つの樽で完結するシングルカスクは、他の樽の中身を覗きません」

一拍の間。

「けれどそれは、孤立しているのではなく——自分の中に必要なものを持っている、ということです」

必要なものだけで、完結する。

視線がカウンターを滑った。取り置きボトル。三席分の距離。

八回通った。毎回確認するようになった。近づいていることを、もう自分に嘘をつかない。

何も考えずに手を伸ばしていた。カウンターの木目に指を滑らせるように——麻布に指先が触れるか触れないかの距離で。

「まだ、早いですよ」

穏やかだが、有無を言わせない声だった。怒られたのとは違う。ただ「まだ」だと言われた。

まだ、ということは——いつかは。

手を引いた。指先に、麻布の感触は残っていない。触れなかった。けれど何かの温度だけが、指先に残っている気がした。

マスターがカウンターの下から何かを取り出して、ペンを短く走らせるのが目の端に映った。前にも見たことがある。お客さまの好みの記録——ネクタイさんの好みだろうか。

「マスター、おやすみなさい」

「おやすみなさいませ。お気をつけて」

扉を押した。路地裏の夜風が指先に冷たい。

帰り道、マスターの言葉が耳の奥で響いた。知る必要のないことは、知らないほうがいい。直接の友人とだけ話せ。

——でも私は今夜、手を出しかけた。あのボトルに。

まだ早いと言われた。

けれど——知りたい、と思った。

それも、嘘ではない。

歩き出した。指先はまだ、あの一瞬の温度を覚えている。


🥃 マスターのテイスティングノート

本日の銘柄: 余市 シングルカスク
お客さまの症状: デメテルの法則違反(Law of Demeter / Message Chains)

ノージング(香り)── 問題の検知

$self->order->customer->address->city のように、矢印(->)が三つ、四つと連なるメソッドチェーンが見えたら、デメテルの法則違反を疑いましょう。「辿れば取れる」の便利さの裏に、本来知る必要のない相手の内部構造への依存が隠れています。

パレット(味わい)── 問題の本質

問題はドットや矢印の数ではありません。中間オブジェクトの構造を暗黙に前提としていることが問題です。Customer が addressprimary_address に変えたとき、OrderReport まで壊れる——知る必要のないことを知ってしまったから。直接の友人を超えた「見知らぬ者」との会話が、変更の波及範囲を爆発させます。

フィニッシュ(余韻)── 解決の方針

Moo の handles で、各クラスが直接の隣人にだけ委譲する構造に変えます。OrderReport は Order だけを知り、Order は Customer だけを知る。Address の変更は Customer の handles で吸収され、Order 以降には波及しません。影響範囲が局所化される——知らないことは、壊れないのです。

ペアリング(相性の良いパターン)

  • Tell, Don’t Ask(聞くな、伝えよ)
  • Facade パターン(複雑なサブシステムへの統一窓口)
  • Feature Envy との区別(データを羨むことと、構造に立ち入ることの違い)

「一つの樽で完結するシングルカスクは、他の樽の中身を覗きません。——けれどそれは孤立ではなく、自分の中に必要なものを持っているということです」

comments powered by Disqus
Hugo で構築されています。
テーマ StackJimmy によって設計されています。