Featured image of post コードバーテンダー【Feature Envy】他人のグラスに手を伸ばす客〜便利という名の依存〜

コードバーテンダー【Feature Envy】他人のグラスに手を伸ばす客〜便利という名の依存〜

Feature Envyとは何か? Perl+Mooのコード例で、他クラスのデータばかり触るメソッドの問題と、Move Method・委譲(handles)による解決策を物語形式で解説します。

前回の夜が、どこか頭に残っていた。だから一週間ぶりに、あの路地裏の扉をもう一度押した。

「お戻りでしたか」

マスターが、少しだけ柔らかい声でそう言った。覚えていてくれたらしい。同じカウンター、同じ壁一面のボトル。先週と変わらない光景に、少しだけ安心する。

「今夜も、おすすめを」

まだ自分で選べない。それは前回と同じだった。

来店——俺のやり方

カウンターに座って棚を眺めていると、扉が開いた。

今度は若者じゃなかった。ジャケットの袖を捲り上げた中年の男性が、しっかりした足取りで入ってくる。カウンターの椅子にどっかりと座り、棚を一瞥した。

「ビールでいいです。——あ、ウイスキーの店ですか。じゃあハイボールで」

「かしこまりました」

マスターがハイボールを作り始めた。グラスに氷を入れ、ウイスキーを注ぎ、ソーダを静かに足す。男性はハイボールを一口飲んで、不機嫌そうに口を開いた。

「あのね、今日コードレビューで後輩に指摘されたんですよ。15年やってきて、初めてですよ、こんなこと言われたの」

15年。私の会社のエンジニアの誰よりもキャリアが長い。ベテランさん——と、すぐにそう思った。堂々とした佇まいに納得がいく。マスターは黙って聞いている。

「俺のクラスのメソッドが、別のクラスのデータを使いすぎてるって。だから何だっていうんですか。使えるデータはまとめて使ったほうが効率いいでしょ」

ベテランさんがスマートフォンを取り出し、画面をカウンターに置いた。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
package ReportGenerator;
use v5.36;
use Moo;

sub generate_order_summary ($self, $order) {
    my $customer_name = $order->customer_name;
    my $item_count    = $order->item_count;
    my $total_price   = $order->total_price;
    my $tax           = $order->tax_amount;
    my $discount      = $order->discount_rate;

    my $final = $total_price * (1 - $discount) + $tax;

    return sprintf(
        "注文者: %s\n商品数: %d\n合計: ¥%.0f",
        $customer_name, $item_count, $final,
    );
}

ReportGeneratorっていうクラスにgenerate_order_summaryってメソッドを書いて、注文データからレポートを作ってるだけです。$orderから必要な情報を取ってきて、整形して返す。何がおかしいんですか」

私はコードの中身はわからなかったけれど、$order->という文字が何度も並んでいるのは見て取れた。

ふと、自分の会社のことが頭に浮かんだ。先週のことがあったからか、「クラス」という言葉だけは覚えていた。

「うちのOrderクラス、顧客情報も在庫情報も全部持ってて。便利よね」

なぜそんなことを口にしたのか、自分でもわからない。

マスターが水差しを手に取り、私のグラスにほんの一滴を落とした。静かな所作だった。何の意味があるのかは、わからなかった。

テイスティング——いろんな顔を持つお酒

マスターが私の前にグラスを置いた。先週の琥珀色とは違う、少し明るい金色だった。

「ジョニーウォーカー グリーンラベルでございます。スコットランドの4つの蒸留所——それぞれ個性の異なる原酒を、一つのボトルにブレンドしたウイスキーです」

一口含んだ。先週のグレンファークラスほど強烈ではないけれど、飲むたびに味が変わる。甘み、スモーキーさ、果物のような香り。不思議なお酒だった。

「4つも蒸留所を使うの? 一つの蒸留所だけじゃだめなの?」

「だめではありません。ただ——」マスターは少し間を置いた。「他の蒸留所の原酒に手を伸ばしすぎると、自分の味がわからなくなることがあります」

マスターの視線がベテランさんのほうに移った。

「お客さま、先ほどのコードを拝見してもよろしいですか」

ベテランさんが画面を差し出す。マスターはしばらく眺めてから、静かに言った。

generate_order_summaryというメソッドですね。ReportGeneratorクラスに置いてあります。——しかし、このメソッドが触っているデータは、すべて$orderのものです」

「だから何です? データが向こうにあるんだから、取りに行くのは当然でしょう」

ベテランさんの声には、苛立ちが混じっていた。

マスターがグラスを一つ持ち上げて、ベテランさんの前に置いた。

「お客さまのグラスがここにあるのに、隣のお客さまのグラスに手を伸ばして飲もうとしたら——どうなるでしょうか」

「……喧嘩になりますね」

「この状態を、Feature Envyと呼びます。メソッドが、自分のクラスよりも他のクラスのデータを羨ましがっている状態です」

ベテランさんの眉がぴくりと動いた。が、すぐに腕を組み直した。

「比喩としてはわかりますけど、コードの話でしょう。動いてるものを直す理由がわからない」

私はつい横から口を挟んでいた。

「ねえ、でも——全部自分のところにまとめたほうが便利じゃない? うちの会社でも、一つの部署で全部やれたら楽なのにって思うもの」

ベテランさんが私のほうを向いた。「そうでしょう? この方のほうが話がわかる」

マスターが一拍だけ黙った。それから、穏やかに言った。

「便利と依存は、似た味がしますね」

その一言が、なぜか引っかかった。ベテランさんも口を閉じた。

マスターがベテランさんの画面にもう一度目を落とした。

「お客さま。もしOrderクラスのtotal_priceが、明日subtotalに変わったら——このメソッドはどうなりますか」

「……壊れますね。$order->total_priceを呼んでるから」

「ええ。自分のクラスなのに、他人の変更で壊れる。隣のグラスに手を伸ばしていると、隣の方が席を立つだけで、こちらのグラスも倒れます」

ベテランさんの表情が変わった。マスターは続けた。

「テストを書くときも同じです。このメソッドを検証するには、Orderのモックを作り、5つのアクセサをすべてスタブしなければならない。自分のメソッドなのに、他人の道具がなければテストもできません」

「……テストは確かに面倒なんですよね」

ベテランさんの声が静かになった。マスターの比喩ではなく、自分自身の実感から出た言葉だった。

ベテランさんがハイボールに目を落とした。

「要するに、このメソッドは——居場所を間違えてる、ってことですか」

「ええ。このメソッドは、Orderクラスにいたがっているとも言えます」

ブレンド——自分のグラスを飲む

「解決の第一歩は、メソッドを適切なクラスに移動することです。Move Methodと呼ばれるリファクタリングです」

マスターはカウンターの下からテイスティングノートを取り出し、何かを書き留めてから、ベテランさんに向き直った。

「先ほどのメソッドの中身を、Orderクラスに移動します」

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

has customer_name => (is => 'ro', isa => Str);
has item_count    => (is => 'ro', isa => Int);
has total_price   => (is => 'ro', isa => Num);
has tax_amount    => (is => 'ro', isa => Num);
has discount_rate => (is => 'ro', isa => Num);

sub summary ($self) {
    my $final = $self->total_price * (1 - $self->discount_rate)
              + $self->tax_amount;

    return sprintf(
        "注文者: %s\n商品数: %d\n合計: ¥%.0f",
        $self->customer_name, $self->item_count, $final,
    );
}

ベテランさんが身を乗り出した。「え、ReportGeneratorがなくなるんですか?」

「なくなるのではありません。メソッドが本来いるべき場所に帰るだけです。加水と同じです——水は外から注ぐものですが、ウイスキーと混ざった瞬間に、その一杯の一部になる。データと振る舞いが同じクラスに収まるとは、そういうことです」

私はベテランさんの画面を覗き込んだ。さっきのコードと何が違うのか——と思って、気づいた。

「あ。$self->ばっかりになってる。さっきは$order->ばっかりだったのに」

ベテランさんが驚いた顔で私を見た。「お姉さん、エンジニアじゃないですよね?」

「違うけど、並んでる文字が変わったのはわかるわよ」

マスターが小さくうなずいた。

「まさにそこです。$order-> の羅列が $self-> に変わった」

マスターはベテランさんに向き直った。

「先ほどのtotal_priceの話を覚えていますか。subtotalに変えたくなっても——」

「同じクラスにあるから、一緒に直せる」

ベテランさんが引き取った。マスターがうなずく。

「テストも、Order->new(customer_name => 'テスト', ...)->summary と呼ぶだけで済みます」

「モックが要らなくなるのか……」

ベテランさんの声に、ようやく納得の色が混じった。

「注文の要約はOrderが作る。データを持ってるやつが、いちばんよく知ってるわけだ」

マスターが静かにうなずいた。

「自分のグラスの中身は、自分が一番よく知っています」

だが、ベテランさんはすぐに別の疑問を口にした。

「でも、ReportGeneratorからOrderの情報を使いたいときはどうするんです。全部移動するわけにもいかないでしょう」

「いい質問です。Mooには委譲という仕組みがあります」

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
package ReportGenerator;
use v5.36;
use Moo;

has order => (
    is      => 'ro',
    handles => ['summary'],
);

sub generate ($self) {
    return "=== 注文レポート ===\n" . $self->summary;
}

handles => ['summary']と書くことで、ReportGenerator$self->summaryと呼べば、内部的に$self->order->summaryが実行されます」

ベテランさんがじっとコードを見ている。

$order->customer_nameとか$order->tax_amountとか、一個一個手を突っ込む必要がなくなる……」

「ええ。他人のグラスに手を突っ込むのではなく、『この一杯をください』と注文する。それが、バーの作法です」

ベテランさんは長い沈黙の後、ハイボールを一口飲んだ。

「……後輩が正しかったってことか」

「後輩の方は、よく嗅覚が働いています」

ラストオーダー——便利の正体

ベテランさんが立ち上がった。来たときの不機嫌さは消えて、何かが腑に落ちた顔をしていた。

「月曜に後輩に謝ります。——で、一緒にリファクタリングしますよ」

カウンターに代金を置いて、ベテランさんはふと私のほうを振り返った。

「お姉さん。あの『並んでる文字が変わった』って気づき、けっこう鋭いですよ」

「えっ、そう? ……ありがとう」

扉が閉まって、二人きりになった。

ジョニーウォーカー グリーンを傾けながら、さっきの話を反芻していた。

「マスター。便利と依存の話、ちょっとだけわかるかも」

マスターが静かにこちらを向いた。

「うちの会社でも、なんでも営業部に頼んじゃうの。顧客情報の確認も、見積りの計算も、在庫の問い合わせも。楽だけど——営業部がパンクしかけてるのよね」

自分で言ってから、なぜコードの話で会社のことを思い出したのか、不思議だった。

マスターは水差しを手に取り、私のグラスにほんの少し加水した。

「——いかがでしょうか」

一口含んだ。さっきと味が違った。同じお酒なのに、甘みの奥にスモーキーな輪郭が浮かび上がっている。

「……おいしい。さっきより、味がはっきりする」

「適切な距離が、味を開かせることもございます」

カウンターの端に目をやった。先週もあった、麻布をかぶったボトル。

「あのボトル、先週もありましたよね」

「ええ。まだ、お取り置きのままです」

穏やかな微笑みだけで、それ以上は何も言わない。

マスターがテイスティングノートに何かを書き留めている。私はもう訊かなかった。きっと、お客さまの好みの記録だろう。

店を出ると、夜風が少しだけ肌寒かった。前回より、あのお酒の味がわかった気がする——ほんの少しだけ。


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

本日の銘柄: ジョニーウォーカー グリーンラベル
お客さまの症状: フィーチャーエンビー(Feature Envy)

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

メソッドの中で$selfよりも$other->のアクセサ呼び出しが多いなら、このアンチパターンを疑いましょう。自分のクラスのデータをほとんど使わずに、他のクラスのデータばかり取りに行くメソッド——それは、隣の客のグラスに手を伸ばしている状態です。

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

Feature Envyの根本は「データと振る舞いの分離」です。データを持つクラスと、そのデータを使うメソッドが別々のクラスにある。この分離が、変更の連鎖(他クラスの属性変更で壊れる)、テストの困難(モック地獄)、責任のねじれ(誰がこの処理を担うべきか曖昧)を引き起こします。

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

Move Methodで、メソッドをデータのあるクラスに移動するのが第一歩。$order->customer_nameの羅列が$self->customer_nameに変わり、変更が閉じ、テストが直接書けるようになります。呼び出し側が必要なら、Mooのhandlesで委譲を使うことで、他クラスの内部構造に手を突っ込まずに機能を借りられます。

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

  • Move Method —— Feature Envy 解消の定番リファクタリング
  • Moo の handles(委譲) —— 他クラスの機能を安全に借りる仕組み
  • Single Responsibility Principle —— データと振る舞いを同じ責任単位に凝集させる

「便利と依存は、似た味がしますね」

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