四回目ともなると、路地裏に足が勝手に向かう。
仕事帰り、駅への道を歩いていたはずが、気がつけばあの薄暗い路地の入り口に立っていた。看板のない店。曲がり角の先にある重い木の扉。先週も、その前の週も、そのまた前の週もここに来た。
「通ってるなあ、私」
独り言が白い息になった。春の夜はまだ冷える。コートの襟を立てて、もう迷わない足取りで路地を抜ける。扉を押すと、いつものカウンター。磨き上げられた木の天板に暖色の照明が落ちている。
「いらっしゃいませ」
マスターの穏やかな声に、ほっとする自分がいた。帰るべき場所がもう一つ増えたような——そんな感覚は、まだ大げさだろうか。
来店——樽が決める味
いつもの席に座る。三回通えば「いつもの席」ができる。マスターもそれを知っていて、カウンターの上におしぼりが置いてある。
「えっと……先週のあの、ボウモアでしたっけ? あれを」
初めて自分から銘柄を口にした。正確に覚えているかどうかは怪しいけれど、二十五年熟成のあの深い琥珀色は印象に残っている。重くて甘くて、少し煙たい味。
マスターが一瞬うなずいた。
「ボウモアでございますね。——ですが、今夜は別のものをお勧めしてもよろしいですか」
「……お願いします」
選ぼうとした。でも、やっぱりマスターに委ねてしまう。自分で注文する日が来るのだろうか。
マスターが棚からボトルを取り出し、グラスに注いだ。先週のボウモアとは明らかに違う。もっと赤い。濃い琥珀色の中に、赤ワインのようなルビーが溶けている。
「グレンドロナックでございます。シェリー樽の影響が非常に強いウイスキーです」
香りを嗅ぐと、まずレーズンの甘さが鼻に飛び込んできた。その奥にチョコレートのような苦みと、オレンジの皮のような柑橘。先週のボウモアの煙たさとはまるで別の世界だ。
「甘い……けど深い。先週のとは全然違う」
「同じ麦から作った原酒でも、熟成に使う樽で味は一変します」
マスターがシェリー樽とバーボン樽の違いを、ざっくりと教えてくれた。同じ原酒が、器によって別のウイスキーになる。
「樽の選択が味を決めます。合わない樽に入れてしまえば——どれだけ良い原酒も、その力を発揮できません」
カウンターの端に目がいく。取り置きボトル。麻布をかぶったあのボトルは、先週よりまた少し近くなっている気がする。今夜はもう聞かなかった。どうせはぐらかされるのだから。でも視界の隅に入るたびに、何か意味があるような気がしてならない。
扉がゆっくり開いた。
スーツのジャケットを腕にかけた男性が入ってきた。メガネの奥の目が疲れている。眉間にしわが寄っていて、難しいことを考えすぎて消化不良を起こしているような——そんな顔だった。
マスターが穏やかに声をかけた。
「いらっしゃいませ。今夜は何をお召し上がりになりますか」
「……何でもいいです。あ、すみません。ハイボールで」
悩めるくん、と心の中で呼んだ。真面目そうで、でもどこか自信がなさそうな。うちの若手エンジニアの一人に似ていた。
ハイボールを一口飲んで、少し間があった。それから右手でメガネをちょっと直してから、ぽつりと話し始めた。
「あの……社内のフレームワークの設計について、相談できるところだと聞いて来たんですけど」
マスターが穏やかにうなずいた。悩めるくんはタブレットを取り出して、画面をカウンターに向けた。
「うちの会社、すべてのモデルクラスが BaseEntity っていう基底クラスを継承するルールなんです」
私は「基底クラス」が何のことかよくわからなかったけれど、「全部のモデルが一つの土台から作られる」ということは、なんとなく理解できた。
「でも、僕が作った ReadOnlyReport は読み取り専用のレポートで、save とか delete とか update とか——親クラスの機能を使わないんです」
「使わないのに、全部もらっちゃうの?」
私の素朴な疑問に、悩めるくんがうなずいた。
「そうなんです。だから空のメソッドにしたり、呼ばれたらエラーを出すようにしたりして……潰して回ってるんです」
そう言って見せてくれたコードがこれだった。
| |
| |
「先週、別のチームの人がこの ReadOnlyReport に save を呼んで——本番でエラーが出ました。BaseEntity のつもりで使ったんです。形の上では BaseEntity だから」
声が沈んでいた。自分のせいだと思っているらしい。
「設計がおかしいと感じてたんですけど……これでいいんでしょうか。社内のルールだから従ってたんですが」
私はうちのエンジニアを思い出した。先月の1on1で「基底クラスの話」をされた。そのときは何のことかわからなくて流してしまったけれど。
「あ、うちもそういう話聞いたことある。うちのエンジニアが、基底クラスの半分を空でオーバーライドしてるって言ってたの。そういうものなんでしょ?」
何気なく言った。ゲスト客の技術の話が、自分の会社のことと重なって、口から出てしまっただけだ。
テイスティング——拒否された遺産
マスターがタブレットに目を通してから、静かに口を開いた。
「ReadOnlyReport が BaseEntity を継承している。しかし、save、delete、update——親から受け継いだ3つのメソッドを、すべて拒否していますね」
悩めるくんがうつむいた。「はい……save と delete は die で止めて、update は中身を空にしてあります。使わないので」
「この状態を、Refused Bequest と呼びます」
マスターの声は穏やかだったけれど、名前の響きには重みがあった。
「日本語では《拒否された遺産》。親から受け継いだものを、子が——使えない。あるいは、使いたくないという状態です」
拒否された遺産。なんだかドラマのタイトルみたいだ。でも悩めるくんのコードを見ると、確かに「遺産を拒否している」としか言いようがない。
「これ、名前があったんですか」
悩めるくんが驚いた顔をした。設計がおかしいと感じていたのに、それが名前のつく問題だとは思っていなかったらしい。そういうものなのか。名前がつくと、問題から課題に変わる——前にマスターがそんなことを言っていた気がする。
なぜ問題なのか
マスターが指を三本立てた。三回目の来店から、この「三つの理由」が始まるとわかるようになった。
「Refused Bequest が問題になる理由は、3つございます」
「まず、リスコフの置換原則——LSP——の違反です」
私にはアルファベット三文字の意味がわからなかったけれど、マスターの説明はわかりやすかった。
「BaseEntity として使えるはずの ReadOnlyReport が、save を呼ばれると例外を投げる。呼び出す側は BaseEntity のつもりで使っているから、実行するまで壊れていることに気づけない。先日の本番障害は、まさにこれが原因です」
悩めるくんが膝の上で拳をぎゅっと握った。「……あの障害、そういう名前がつく問題だったんですね」
「次に、偽の契約です。extends BaseEntity という宣言は、《私は BaseEntity のすべてを引き受けます》という約束です。実際には半分を拒否している。型が嘘をついている状態でございます」
私が口を挟んだ。「嘘ってこと? コードが嘘をつくの?」
マスターがこちらを向いた。
「ええ。ReadOnlyReport は《BaseEntity です》と名乗っていますが、BaseEntity としての振る舞いを果たせません。名刺に書いてある肩書きと実際の仕事が違う——そう考えていただければ」
なるほど。それはビジネスの世界でも、信用を失う最短ルートだ。
「最後に、継承階層の汚染です。BaseEntity に新しいメソッドが追加されるたびに、ReadOnlyReport でもオーバーライドが必要になります。親が成長するほど、子の拒否リストが膨らんでいく」
悩めるくんが深いため息をついた。
「3番目、まさにそれです。先月 BaseEntity に archive メソッドが追加されて……また空のオーバーライドを書きました」
マスターがグレンドロナックのボトルを少し傾けた。深いルビー色の液体が光を受ける。
「樽が合わないのに、無理に注ぎ続けている。味は出るどころか、新しく注ぐたびに濁りが増していくのです」
私は悩めるくんに聞いた。隣の席の彼が不機嫌そうに見えたので心配だったのだけれど、不機嫌なのではなく困惑しているのだとわかった。
「ねえ、それっておかしいと思ってたんでしょ? 作ってるときから」
悩めるくんが顔を上げた。「……はい。空のメソッドを書くたびに、何か違うなって。でも——」
「社内のルールだから?」
「はい。すべてのモデルは BaseEntity を継承する。そう決まってるので」
私は首をかしげた。経営者としては、その考え方にひっかかるものがある。
「合わないとわかってるのに使い続けるの? うちの会社だったら、合わない仕組みは変えるけど」
悩めるくんが一瞬、言葉を失った。
マスターが何も言わなかった。カウンターを拭く手も止まっていなかった。ただ、ほんの一拍——マスターの視線がどこかに向いて戻ったような気がした。
ブレンド——器を変えるブレンダーの仕事
マスターが口を開いた。
「解決策は、器を変えることです」
私は「樽の選択が味を決める」という先ほどの言葉を思い出した。合わない樽なら、樽を変えればいい。
「継承で丸ごと受け取っていたものを、必要なものだけ選んで組み合わせる。ウイスキーのブレンドと同じです——原酒を丸ごと使うのではなく、必要な味だけを取り出して調合します」
解決策1: Role による合成
「まず、BaseEntity が持っていた《識別する》という性質を、Roleとして切り出します」
マスターがタブレットに新しいコードを表示した。
| |
「Moo::Role——共通の《性質》を定義するための仕組みです。《私は識別できます》という宣言であり、《私はすべてを引き受けます》という宣言ではありません」
| |
悩めるくんが画面を見比べた。
「extends が with に変わっただけですか?」
「似ているようで、構造が根本的に違います」
マスターの声がわずかに力を帯びた。この人が語気を強めるのは珍しい。それだけ重要なことなのだろう。
「extends は《すべてを受け取る》宣言です。save も delete も update も、望むものも望まないものも全て。一方の with は——《必要なものを選んで合成する》宣言です。id と name だけを手に入れて、それ以外は持たない」
私が思わず口を挟んだ。
「全部引き取る遺産相続と、必要なものだけもらう形見分けみたいなもの?」
マスターが——一瞬、何かを言いかけて止まった。それから穏やかな驚きの表情を浮かべた。いつだって冷静なマスターの目が、ほんの少しだけ丸くなった気がした。
「……見事な例えでございます」
ちょっと嬉しかった。マスターに褒められたのは初めてかもしれない。
なぜこれで問題が消えるのか
「3つの問題が、すべて構造的に消えています」
マスターは先ほどと同じように指を三本立てた。
「まず、LSP 違反が消えます。ReadOnlyReport は BaseEntity ではないので、BaseEntity として扱われることがありません。save を呼ぼうとしても、メソッドがそもそも存在しないため、呼び出した瞬間に《メソッドが見つからない》という明確なエラーが出ます。カスタムの die メッセージではなく、Perl が標準で出すエラーです。原因の特定が格段に容易になります」
悩めるくんが小さくうなずいた。「先日の障害は……ReadOnlyReport 独自の die メッセージだったから、最初は何が起きたのかわからなかった。メソッドが存在しなければ、エラーメッセージそのものが原因を教えてくれる」
「ええ。問題を《カスタムエラーで隠された障害》から《メソッド不在という明確なエラー》に変えた。それが構造を変える効果です」
「次に、偽の契約が消えます。with 'Identifiable' は《私は識別可能です》という正直な宣言です。嘘がありません」
「最後に、継承階層の汚染が消えます。BaseEntity に何が追加されても、ReadOnlyReport には影響しません。独立しているからです。archive メソッドがいくつ追加されようと、空のオーバーライドを書く必要は二度とございません」
悩めるくんの眉間のしわが、少しだけ——ほんの少しだけ緩んだように見えた。
解決策2: 委譲による公開制限
「もう一つの方法もございます」
マスターが続けた。
「既存の BaseEntity を変更できない場合——たとえば、社内の他のチームがそのクラスを管理していて、勝手に触れない場合には、委譲という手段があります」
| |
「handles で公開するメソッドを明示的に選ぶ。id と name だけを外に見せて、save も delete も update も——内側に閉じ込めます」
「樽の中から、使いたい味だけを取り出す。ブレンダーの仕事そのものです」
悩めるくんが考え込むような表情で、二つのコードを見比べていた。
「Role と委譲、どちらを使うべきでしょうか」
「共通の《性質》を表現するなら、Role。既存の《部品》を活かすなら、委譲です」
マスターが手元のグレンドロナックのボトルと、棚の奥のもう一本を交互に見た。
「お客さまの場合、BaseEntity を設計から見直せるなら Role が適しています。社内のルールで BaseEntity に手を入れられないなら、委譲で包んでしまえばよい。——どちらの道を選ぶかは、お客さまの状況次第です」
ラストオーダー——器を変える勇気
悩めるくんが立ち上がった。来たときより、背筋が少し伸びていた。
帰り際、私のほうを向いた。
「ありがとうございます。あの一言で、目が覚めました」
「え、私? 何か言った?」
「合わない仕組みは変えるもの——技術の話じゃなくて、そういう話だったんだなって」
頭を小さく下げて、扉の向こうへ消えていった。
私は面食らった。ただの独り言みたいなものだったのに。エンジニアの世界と経営の世界で、同じことが起きているとは思わなかった。
マスターがグレンドロナックのグラスを下げながら、静かに言った。
「器を変える勇気も、ブレンダーの大切な仕事でございます」
ブレンダーの仕事。合わない樽では味が出ない。合わない継承では——コードの力が出ない。マスターはそう言いたかったのだろうか。
ふと、スマホを取り出した。
今夜の話をメモしておきたい、と思った。「Refused Bequest」「器を変える」——親指で打ち始めたけれど、何をどう書けばいいのかわからなかった。あの悩めるくんのコードの何がまずくて、Role というものがどう解決したのか。わかったつもりでいて、言葉にしようとすると指が止まる。
画面を見つめたまま、結局スマホを閉じた。
マスターがそれを見て、穏やかに微笑んだ。カウンターの同じ場所を二度拭いてから、棚のボトルに手を伸ばした。
「知りたい」という気持ちは、たぶん「わかる」の手前にある。今の私は、その手前にいるのだろう。
帰り支度をして扉に向かう。背中の向こうで、マスターが取り置きボトルの位置をずらす微かな音が——したような気がした。振り向いたけれど、マスターはカウンターを拭いているだけだった。
「おやすみなさいませ」
静かな声を背中に受けて、路地裏へ出た。春の夜風がまだ少し冷たい。でも帰り道は、いつもより少しだけ短く感じた。
🥃 マスターのテイスティングノート
本日の銘柄: グレンドロナック
お客さまの症状: 拒否された遺産(Refused Bequest)
ノージング(香り)── 問題の検知
親クラスから継承したメソッドを空でオーバーライドしている、あるいは die でエラーを返しているコードを見つけたら、Refused Bequest を疑いましょう。extends しているのに親の機能を半分以上使っていなければ、その継承関係は正しくありません。
パレット(味わい)── 問題の本質
extends は「すべてを引き受ける」宣言です。親のメソッドを空でオーバーライドするということは、型が嘘をついている状態です。呼び出す側は親クラスのつもりで使うため、実行するまで壊れていることに気づけません。さらに、親クラスに機能が追加されるたびに、子クラスの拒否リストが膨らんでいきます。
フィニッシュ(余韻)── 解決の方針
extends(継承)を with(Role合成)または has + handles(委譲)に置き換えます。Role なら共通の「性質」だけを選んで合成でき、委譲なら既存クラスの必要なメソッドだけを外部に公開できます。いずれの場合も、不要なメソッドは「存在しない」状態になり、カスタムの die による曖昧なエラーが、メソッド不在という明確なエラーに変わります。
ペアリング(相性の良いパターン)
- Composition over Inheritance(GoF)
- リスコフの置換原則(SOLID の L)
- Strategy パターン(振る舞いの差し替えが必要な場合の代替手段)
「樽の選択が味を決めます。——器を変える勇気もまた、ブレンダーの大切な仕事でございます」
