Featured image of post コードバーテンダー【Primitive Obsession】加工しない原酒の力強さ〜型なき世界の代償〜

コードバーテンダー【Primitive Obsession】加工しない原酒の力強さ〜型なき世界の代償〜

Primitive Obsession(プリミティブ型への執着)とは何か? Perl+Mooのコード例で、文字列と数値ですべてを表現する問題と、Value Objectによる解決策を物語形式で解説します。

六杯目を、自分で選ぼうと思った。

いつもの席に座って、壁一面のボトルを見上げる。グレンファークラス、ジョニーウォーカー、ボウモア、グレンドロナック、白州——五回分の記憶が、ラベルの向こうにある。どれもマスターが選んでくれたものだった。

今夜は違う。理由はない。ただ、六回も通えば自分の好みくらいわかっていいはずだ。

棚の中段に、目を引くボトルがあった。ラベルには黒地に金の文字。どっしりとした暗い緑のガラス瓶。なんとなく力強そうだ。

「あれを」

指差した。マスターが私の指先を追って棚を見上げ——それからこちらを見た。一拍の間があった。いつもならすぐに返事をくれるのに。

「かしこまりました」

グラスに注がれた液体は、琥珀というより黒蜜に近い濃さだった。粘り気がある。グラスの壁をゆっくり伝い落ちる。

アードベッグ ウーガダールでございます。アルコール度数は54.2%。カスクストレングス——樽から出したままの度数でございます」

鼻を近づけた。煙の向こうに何かがある。甘い? でもまず煙が来る。ピート。前にマスターが教えてくれた言葉だ。

一口、含んだ。

喉が、焼けた。

煙と一緒にアルコールの刺激が鼻を突き抜けて、甘さなんてどこかに吹き飛んでしまった。咳き込んで、思わずグラスをカウンターに置いた。

「きつい……!」

目に涙が浮かんでいる。恥ずかしい。

マスターは怒りもせず、呆れもせず、穏やかに言った。

「カスクストレングスは、加工しない原酒の力強さをそのまま味わえるウイスキーです。——しかし、そのままでは飲む方を選びます」

マスターがスポイトを取り出して、グラスに一滴だけ水を垂らした。

「少々お待ちください」

「え、一滴だけ……?」

マスターは微笑んだだけで答えなかった。

しばらく待って、もう一度口をつける。

——あれ。

さっきのアルコールの暴力が嘘のように引いて、その奥にダークチョコレートの苦みと、レーズンの甘さが顔を出した。煙はある。でもその煙が、今度は味を覆い隠すのではなく、味を引き立てるフィルターのように機能している。

「全然違う。同じお酒なの、これ?」

「同じ原酒でございます。加水というほんの少しの加工が、隠れていた味を引き出します」

同じ液体。同じ原酒。なのに一滴の水で、まるで別の飲み物になった。私は自分で選んで、いきなり原酒をストレートで飲もうとしたのだ。選ぶことと、正しく選ぶことは違う。

カウンターの端に目がいった。取り置きボトル。あの麻布をかぶったボトルが——今夜はいつもと違った。麻布がずれている。ほんの少し下にずれて、瓶の下のほうが見えていた。照明を受けて、琥珀色の液体が光っている。

「……綺麗な色」

マスターの手がカウンターの上で止まった。それからいつもの調子で答えた。

「まだ仕上がっていません」

穏やかだけど、きっぱりしていた。「仕上がっていない」。何を仕上げているのだろう。でもこのボトルが少しずつ私に近づいてきていることと、今夜初めて中身が見えたことは——何かの変化なのだと思う。

来店——荒い原酒と自由の代償

扉が軽快に開いた。

大きなトートバッグを肩にかけた女性。ノートPCが覗いている。パーカーにスニーカー。今夜のバーの客層——といっても私とマスターしかいないけれど——からすると、ずいぶんラフな格好だ。でもそれがよく似合っている。肩の力が抜けていて、自分の足で歩いている感じがする。

「こんばんは。ここがコードの相談できるバーって聞いたんだけど。面白そうじゃない」

フリーランスさん、と心の中で呼んだ。会社に所属していない人の空気がある。自由で、軽くて、でも一人で全部背負っている強さみたいなもの。

マスターが声をかけた。

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

「ハイボールで。あ、スモーキーなやつがあれば嬉しい」

注文の仕方にも迷いがない。いいな、と思った。私は六杯目でようやく自分で選んでみて、咳き込んだのに。

フリーランスさんはハイボールをぐいっと飲んで、すぐに本題に入った。

「あたし、フリーランスで15年やってるの。最近バグ報告が続いてて——コード自体は動いてるのに、データがおかしいのよね」

「具体的には、どのようなバグでしょうか」

マスターが静かに訊いた。

「メールアドレスのフィールドに電話番号が入ってたり、金額を文字列のまま足し算して桁がずれたり。全部文字列とか数値で持ってるんだけど、何が悪いのかがわかんないのよ」

文字列で持っている——。私は思わず口を挟んだ。

「あ、うちもそう。メールアドレスも電話番号も全部文字列で持ってるけど——でもメールアドレスのところに電話番号が入るって、どういうこと?」

マスターの返事までの間が、一拍だけ長かった。気のせいかもしれない。でも今夜はその「一拍」が妙に引っかかった。

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

「コードを拝見してもよろしいですか」

フリーランスさんがトートバッグからノート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
package CustomerService;
use v5.36;
use Moo;
use Types::Standard qw(Str Int ArrayRef HashRef);

has customers => (
    is      => 'ro',
    isa     => ArrayRef[HashRef],
    default => sub { [] },
);

sub add_customer ($self, $name, $email, $phone, $age) {
    push $self->customers->@*, {
        name  => $name,
        email => $email,
        phone => $phone,
        age   => $age,
    };
}

sub find_by_email ($self, $email) {
    my @found = grep { $_->{email} eq $email }
        $self->customers->@*;
    return $found[0];
}

sub total_ages ($self) {
    my $sum = 0;
    $sum += $_->{age} for $self->customers->@*;
    return $sum;
}

私にはコードの詳しいことはわからない。でも $email とか $phone という名前が見える。それぞれメールと電話のことだろう。

フリーランスさんが胸を張って言った。

StrInt で型もつけてるでしょ? これで十分じゃない? あたし15年このスタイルでやってきたのよ」

マスターが穏やかに訊いた。

「では、この add_customer を呼ぶとき、$email に電話番号を、$phone にメールアドレスを渡したら、どうなりますか」

フリーランスさんの手が一瞬止まった。

「……動く。エラーにはならない」

「それが、問題でございます」

テイスティング——蒸発した意味

マスターがカウンター越しにフリーランスさんのコードを見ている。指一本触れずに、目だけで読んでいる。

$emailStr 型です。文字列であれば何でも受け入れます。電話番号も、住所も、お名前も——すべて文字列です。つまり $email という名前が意味を持っているのは、プログラマの頭の中だけで、コードはその約束を何も保証していません。——この状態を、Primitive Obsession、プリミティブ型への執着と呼びます」

私は「プリミティブ」という言葉を前にも聞いたことがある気がした。確か一回目の夜、マジックナンバーの話のときに。数字にも名前がつく問題がある、とマスターが言っていた。今度は文字列にも名前がつく問題があるらしい。

「でも、型って制約でしょ?」

私が言った。会社でもいつも考えていることだ。

「会社の規則みたいなもので、多いほど窮屈になるんじゃない? ルールは最小限にしたいのが経営者の本能よ」

フリーランスさんが勢いよく同意した。

「そう! まさにそれ。あたし15年、型に縛られたコードなんて見てると息苦しくなるのよ。Perlは自由に書けるのがいいところじゃない」

わかる。フリーランスさんの「自由にやりたい」は、私の「ルールは最小限にしたい」と同じ匂いがする。一人で全部やってきた人の、軽さと潔さ。

マスターがフリーランスさんに静かに訊いた。

「ご質問させていただいてもよろしいですか。お客さまの案件で、メールアドレスの入力を受け付ける場所はいくつありますか」

フリーランスさんが天井を見上げて指を折った。

「7箇所くらい……かな」

「その7箇所すべてで、メールアドレスのバリデーションを行っていますか」

沈黙。

「……3箇所はやってる。あとの4箇所は、まあ、入力する人がまともなアドレスを入れるでしょ、って」

「3箇所にバリデーションがあり、4箇所にはない。同じ概念を7箇所に散在させて、そのうち4箇所は『信頼に依存』しています」

フリーランスさんの眉がわずかに動いた。図星、という顔ではなかった。でも「あれ」という顔だった。自分でも薄々わかっていたけど、言語化されたのは初めて——そういう表情。

マスターが続けた。

「Primitive Obsession が問題になるのは、3つの理由がございます。第一に、意味の蒸発です。Str は『文字列である』としか言っていません。メールアドレスであること、電話番号であること——ドメインの意味がコードから蒸発しています。プログラマの記憶だけが意味を保持しており、記憶は引き継げません」

フリーランスさんが口を開きかけた。引き継ぎ——フリーランスだから、案件を離れたら次の人が保守する。

「第二に、バリデーションの散在です。同じ制約を、受け入れ口ごとに書く羽目になります。7箇所あれば7回。1箇所でも忘れれば、不正なデータが入り込みます。お客さまのバグ報告は、まさにこのパターンです」

フリーランスさんが視線を落とした。4箇所の穴。自覚はあったのだろう。

「第三に、型の嘘です。$email という変数名は『メールアドレスである』と主張しています。しかし Str 型は電話番号も受け入れます。変数名が嘘をついている。その嘘は検出されないまま本番まで到達します」

フリーランスさんがカウンターに手をついた。

「バリデーション書き忘れってだけの話でしょ。全箇所にバリデーション入れればいいじゃない」

「その方法でも修正はできます」

マスターは否定しなかった。

「——しかし、次に新しい画面を作るとき、また同じバリデーションを書くことを覚えていられますか。そしてその次の人は」

私は7箇所に同じチェックを入れる情景を想像してみた。7箇所——うちの会社にも似たような仕組みがなかったか。そうだ、経費精算。システムが3つに分かれていて、入力チェックがバラバラだって経理の子が愚痴を言っていたな。

マスターが私のグラスを軽く示した。

「さきほどカスクストレングスをお召し上がりいただきましたね」

「はい。最初きつくて死ぬかと思いました」

「加工しない原酒——つまり何の制約もかけない値——は、力強い。しかし、そのままでは味がわからない。一滴の加水というほんの少しの約束が、隠れていた味を引き出しました」

マスターがゆっくりと、でもはっきりと言った。

制約は制限ではなく、約束です。値が守るべき約束を型として表現する——それが解決策です」

ドキッとした。「制約ではなく約束」。私の会社で「ルールは最小限に」と言ってきたけど、それは制約を減らすことであって、約束をなくすこととは違う。もし約束がないまま動いているなら——。

口には出さなかった。でもグラスの中のアードベッグを見つめながら、それだけ考えていた。

ブレンド——約束を型にする

マスターがカウンター越しにフリーランスさんに向き直った。

「解決策は、Value Object の導入です。値に名前を付け、制約を持たせ、不正な状態では生まれてこないオブジェクトにします。まず、メールアドレスを Value Object として定義しましょう」

マスターがフリーランスさんのノートPCのそばに、自分のタブレットを置いた。そこにコードが映っていた。

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

has value => (
    is       => 'ro',
    isa      => Str,
    required => 1,
);

sub BUILD ($self, $args) {
    die "Invalid email address: " . $self->value
        unless $self->value =~
            /\A[^\s@]+@[^\s@]+\.[^\s@]+\z/;
}

sub equal ($self, $other) {
    return ref $other eq ref $self
        && $self->value eq $other->value;
}

フリーランスさんが目を丸くした。

「え、クラスを作るの? 文字列のためだけに?」

「はい。メールアドレスは『文字列』ではありません。形式の約束を持つドメインの概念です。このクラスのポイントは3つあります。第一に、is => 'ro'。一度作ったら変更できません。不変です。メールアドレスが途中で変わったら、それは別のメールアドレスです」

フリーランスさんがコードに目を戻した。

「第二に、BUILD でのバリデーション。不正な値では生まれてこない。このオブジェクトが存在している時点で、有効であることが保証されています。バリデーション忘れは構造的にゼロになります。——そして第三に、名前があります。Str ではなく EmailAddress。変数名に頼らず、型そのものが意味を伝えます」

マスターがタブレットをスクロールした。

「同様に、電話番号と年齢も Value Object にします」

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
package PhoneNumber;
use v5.36;
use Moo;
use Types::Standard qw(Str);

has value => (
    is       => 'ro',
    isa      => Str,
    required => 1,
);

sub BUILD ($self, $args) {
    die "Invalid phone number: must be 10-11 digits"
        unless $self->value =~ /\A\d{10,11}\z/;
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
package Age;
use v5.36;
use Moo;
use Types::Standard qw(Int);

has value => (
    is       => 'ro',
    isa      => Int,
    required => 1,
);

sub BUILD ($self, $args) {
    die "Age must be between 0 and 150"
        unless $self->value >= 0
            && $self->value <= 150;
}

「そして、これらの Value Object を組み合わせて、顧客を定義します」

1
2
3
4
5
6
7
8
9
package Customer;
use v5.36;
use Moo;
use Types::Standard qw(Str InstanceOf);

has name  => (is => 'ro', isa => Str,                      required => 1);
has email => (is => 'ro', isa => InstanceOf['EmailAddress'], required => 1);
has phone => (is => 'ro', isa => InstanceOf['PhoneNumber'], required => 1);
has age   => (is => 'ro', isa => InstanceOf['Age'],         required => 1);

フリーランスさんが腕を組んだ。「InstanceOf ね。で、これをどう使うわけ?」

マスターがもう一つのコードを見せた。

 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
package CustomerService;
use v5.36;
use Moo;
use Types::Standard qw(ArrayRef InstanceOf Str);

has customers => (
    is      => 'ro',
    isa     => ArrayRef[InstanceOf['Customer']],
    default => sub { [] },
);

sub add_customer ($self, $name, $email, $phone, $age) {
    push $self->customers->@*, Customer->new(
        name  => $name,
        email => EmailAddress->new(value => $email),
        phone => PhoneNumber->new(value => $phone),
        age   => Age->new(value => $age),
    );
}

sub find_by_email ($self, $email_str) {
    my $email = EmailAddress->new(value => $email_str);
    my @found = grep {
        $_->email->equal($email)
    } $self->customers->@*;
    return $found[0];
}

sub total_ages ($self) {
    my $sum = 0;
    $sum += $_->age->value for $self->customers->@*;
    return $sum;
}

「コード量は増えてるわよね」

フリーランスさんの指摘は正確だった。

「はい。行数は増えます。——しかし、バリデーションの数は減ります」

マスターが静かに続けた。

「Before では7箇所にメールアドレスの入力があれば、7箇所にバリデーションが必要でした。After では EmailAddress->new が1箇所で検証を完結させます。7箇所のどこで作っても、同じ約束が守られます」

フリーランスさんがしばらく黙ってコードを見つめていた。キーボードの上で指が止まっている。それからゆっくりと口を開いた。

「……ああ、そういうことか」

立ち上がりかけて、座り直した。

「あたしが7箇所にバリデーション入れる代わりに、1個のクラスが全部保証してくれるのね。しかも CustomeremailInstanceOf['EmailAddress'] だから、PhoneNumber を渡そうとしたら——」

「その時点で、エラーになります」

フリーランスさんの目が変わった。さっきまで「面白いじゃない」だった表情に、理解の光が差した。

「取り違えが、存在しなくなる。忘れるとか忘れないの問題じゃなくて、構造的に不可能になる

マスターがゆっくりうなずいた。

「おっしゃる通りです」

私はフリーランスさんの横顔を見ていた。自分で気づいたのだ。マスターに教わったのではなく、コードを見つめて、自分で答えにたどり着いた。15年の経験があるから、一度理解すれば早い。

マスターが続けた。

「3つの問題がどう変わったかを確認しましょう。第一に、意味の復活です。EmailAddress という型が『これはメールアドレスである』と宣言しています。変数名に頼らず、型が意味を保持します。プログラマの記憶が消えても、意味は残ります」

フリーランスさんが小さくうなずいた。

「第二に、バリデーションの一元化です。バリデーションは EmailAddressBUILD に一箇所。7箇所の書き忘れリスクはゼロです。新しい画面を作る人も EmailAddress->new を使えば、自動的に検証されます。——そして第三に、型が嘘をつけないことです。先ほどの Customer で見たように、InstanceOf['EmailAddress']EmailAddress のインスタンスしか受け入れません。電話番号を渡すことは、構造的に不可能です」

私は前にマスターが言った言葉を思い出した。ラベルのないボトル。中身を知る人がいなくなったら、ただの液体になる。今夜の話はその続きだ。

「つまり、ラベルのないボトルにラベルを貼って、中身まで保証するってこと?」

マスターが私を見た。穏やかだけど、少し驚いたような目。

「ええ。そしてラベルが貼られていないものは、棚に並べない。——それが約束です」

ラストオーダー——疑問形の夜

フリーランスさんがトートバッグを肩にかけ直した。ノートPCをしまいながら、カウンターに目を落として言った。

「15年かあ。ずっと自由に書いてきたけど、自由ってのは約束がないことじゃなくて、約束を自分で決められることだったのかもね」

それからこちらを見て、軽く手を挙げた。

「ね、お姉さんの会社もフリーランスに発注してる? 型をちゃんと使ってる人に頼んだほうがいいわよ」

苦笑した。「あたしも型なんて要らないと思ってた側なんだけど」

フリーランスさんは軽く笑って、扉を押した。入ってきたときと同じ軽い足取りで出ていく。でもどこか、さっきより背筋が伸びていた気がする。

カウンターに私とマスターだけが残った。

手元のグラスに目を落とした。マスターが加水してくれたアードベッグ ウーガダール。最初のカスクストレングスの衝撃が嘘のように、煙の奥にチョコレートと果実味が広がっている。同じ原酒。同じ液体。なのに、一滴の水で別物になった。

「マスター、最初の一口、本当にきつかったです」

「加工しない原酒には、力も味も詰まっています」

マスターがグラスを磨きながら言った。

「けれど、適切な器とほんの少しの加工がなければ、その良さは届きません」

器。型のことだろうか。Value Object という器に入れるから、中身が生きる。入れないまま出したら——飲む人が火傷する。さっきの私のように。

「メールアドレスが文字列って、普通だと思ってました」

「ええ。多くの方がそうおっしゃいます」

返事の間。いつもと同じだった。今夜のマスターは、あの一拍の長い沈黙を最初に一度だけ見せて、あとはいつもどおりだった。それがかえって気になった。

ふと思い出した。最初にカスクストレングスを自分で選んだこと。棚を見て「あれを」と指差した。五回分の経験が、「自分で選ぶ」勇気をくれた。でも選ぶことと、正しく選ぶことは違った。

「次は——もう少し上手に選べるかな」

「選ぶこともまた、学びでございます」

帰り支度をするとき、取り置きボトルをもう一度見た。麻布がずれたまま。琥珀色の中身が光っている。

「綺麗な色」

二度目のその言葉に、マスターは答えなかった。カウンターを拭いていた。

路地裏を歩いた。フリーランスさんの言葉が頭に残っている。「全部文字列で持ってるって普通でしょ」。そう、私もそう思っていた。マスターの「約束」という言葉。約束がなくても動いてる。動いてるけど——。

「動いてる……わよね?」

口に出して、自分で驚いた。私は、疑問に思ってしまったのだ。

口に残るアードベッグの煙たさが、まだ消えなかった。


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

本日の銘柄: アードベッグ ウーガダール
お客さまの症状: プリミティブ型への執着(Primitive Obsession)

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

StrInt だけで顧客情報を保持し、メールアドレスと電話番号の取り違えがエラーにならないコード。変数名だけが意味を持ち、型は何も約束していない——そのとき、このアンチパターンを疑いましょう。

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

プリミティブ型は「文字列である」「整数である」としか言っていません。ドメインの意味がコードから蒸発し、バリデーションが受け入れ口ごとに散在します。7箇所に同じチェックを書いて、8箇所目で忘れたとき——不正なデータは静かに本番に届きます。

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

Value Object を導入し、値に名前と制約を持たせます。EmailAddress->new が存在する限り、バリデーションは自動的に適用されます。Before ではプログラマの記憶に依存していた「約束」が、After では型として刻まれ、取り違えは構造的に不可能になります。

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

  • Types::Standard による宣言的な型制約(InstanceOf, StrMatch
  • Introduce Parameter Object(データの群れを一つのオブジェクトにまとめる)
  • Magic Numbers(第1回)との関連——数値のラベルと、文字列のラベル

「適切な器とほんの少しの加工がなければ、原酒の良さは届きません」

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