サーバールーム横の会議室は、空調の風だけが低く唸っている。私はノートPCを開いて、約束の時刻を待っていた。
技術顧問の田中さんが言っていた。「変わった人だが、コードを見せればわかる」と。
10年間、evalの中身を手作業で書き換え続けてきた。先月の本番停止の始末書を書いたとき、さすがに限界だと思った。問題はわかっている。わかっていて、直す余裕がなかった。いや、直し方がわからなかったのかもしれない。
会議室のドアが開いた。
入ってきたのは、手にペンとノートだけを持った男だった。タブレットもノートPCも持っていない。部屋に入るなり、ホワイトボードを一瞥して立ち止まる。
「この部屋の最後の会議は3日前だね。マーカーのインクの乾き具合でわかる」
……ああ、この人か。田中さんの言った「変わった人」というのは、こういうことか。
「ロック。コードの探偵だ。で、君が10年間守り続けてきた呪文を見せてくれたまえ——ワトソン君」
「……好きに呼んでください。それより、呪文って何のことですか」
「evalの中身だよ」
ロックさんはペンを弄びながら、私の向かいに座った。
「evalを使うシステムの運用者は、呪文の暗唱者だ。意味を理解して唱えているのか、音だけ真似ているのか。さて、君はどちらかね」
私は黙ってノートPCの画面を向けた。DBから取得したルール文字列の一覧が映っている。
事件現場——evalの中身
画面に映っていたのは、こんなコードだ。
| |
DBに格納されたルール文字列の例:
| |
キャンペーンのルールが200本以上。すべてが文字列としてDBに保存され、eval で実行されている。マーケティング部門から毎週新しいルールの追加依頼が来る。そのたびに誰かが文字列を手で書いて、DBにINSERTする。
「先月、括弧の閉じ忘れで3時間の本番停止を出しました。始末書も書きました」
ロックさんは画面を覗き込んだまま黙っている。ルール文字列を3つ、4つと目で追っている。長い沈黙だった。
容疑者の浮上——文法なき言語
「……ワトソン君、これは言語だ」
「言語? ただの条件式ですよ」
「いいや。$ctx->{total_amount} >= 10000 && $ctx->{member_level} eq "gold" は条件式に見えるが、200本集まればそれは言語だ。文法のない言語。そしてevalは、文法チェックなしにその言語を実行する偽りの翻訳者だ」
偽りの翻訳者。妙な言い方だと思った。しかし、的を射ている。
「翻訳者は原文の正しさに責任を持たない。括弧が足りなくても、スペルミスがあっても、渡された通りに実行する。先月の3時間の停止は、翻訳者の忠実さがもたらした裏切りだ」
ロックさんはペンをノートに走らせながら続けた。
「犯人はevalではない。文法を定義せずに言語を運用してきた、設計の不在そのものだ」
10年動いてきたシステムだ。evalが悪いのか、と言い切るのは抵抗がある。
「10年動いてきたんですよ。evalが悪いとは言い切れないのでは」
「動いている と 理解できている は別の言葉だよ、ワトソン君。この呪文が10年間正しく唱えられてきたのは、君という祈祷師がいたからだ。——君がいなくなったら?」
返す言葉がなかった。実際、私が異動したら誰がこのルールを書けるのか。引き継ぎ書に「eval文字列の書き方」を残す未来を想像して、背筋が冷えた。
推理披露——Interpreter パターン
ロックさんはノートに木のような図を描き始めた。
「言語が存在するなら、文法を与えればいい。Interpreterパターン——文法規則をオブジェクトの木として表現し、再帰的に評価する」
classDiagram
class Expression {
<<Role>>
+interpret(ctx) bool
}
class Expr_GreaterThan {
+field: Str
+threshold: Num
+interpret(ctx) bool
}
class Expr_GreaterThanOrEqual {
+field: Str
+threshold: Num
+interpret(ctx) bool
}
class Expr_Equals {
+field: Str
+value: Str
+interpret(ctx) bool
}
class Expr_And {
+left: Expression
+right: Expression
+interpret(ctx) bool
}
class Expr_Or {
+left: Expression
+right: Expression
+interpret(ctx) bool
}
class Expr_Not {
+expr: Expression
+interpret(ctx) bool
}
Expression <|.. Expr_GreaterThan
Expression <|.. Expr_GreaterThanOrEqual
Expression <|.. Expr_Equals
Expression <|.. Expr_And
Expression <|.. Expr_Or
Expression <|.. Expr_Not
Expr_And --> Expression : left
Expr_And --> Expression : right
Expr_Or --> Expression : left
Expr_Or --> Expression : right
Expr_Not --> Expression : expr
Expression ロール——文法の契約
「すべての式は、一つの質問に答えられなければならない。『このコンテキストで、お前は真か偽か?』」
| |
「Specificationパターンと何が違うんですか? 条件をオブジェクトにするなら、同じことでは」
先日、社内勉強会でSpecificationパターンの記事を読んだばかりだった。条件をオブジェクトにするという点では似ている。
「Specificationは問いに名前を付ける。Interpreterは問い自体の文法を定義する。法廷で言えば、Specificationは証言者、Interpreterは訴訟手続法だ。evalは手続きなき裁判——つまりリンチだよ」
法廷の比喩は大袈裟だと思ったが、構造上の違いは理解できた。Specificationは「この条件を満たすか」を聞く。Interpreterは「条件の書き方そのものをルール化する」。レイヤーが一段深い。
TerminalExpression——終端式
「文法の最小単位。これ以上分解できない原子的な判定だ」
| |
| |
| |
「$ctx->{total_amount} > 5000 の代わりに Expr::GreaterThan->new(...) ですか。……長いですね」
正直な感想だった。文字列なら1行で書ける条件が、オブジェクトの生成で3行になる。
「呪文を一行で書くか、文法書を一冊用意するか。文法書があれば、新しい呪文は書く前に正しさを検証できる」
NonterminalExpression——非終端式
「終端式を木として組み合わせる糊だ」
| |
| |
| |
「木……構文木ですか。コンパイラの教科書で昔見た記憶があります」
「evalが文字列を直接実行するのは、翻訳なしで外国語の発音だけを真似るようなものだ。構文木は構造化された理解だ。木の各ノードは、自分の責任範囲だけを評価する」
構文木の構築——翻訳は構造に宿る
ロックさんはノートにBefore → Afterの対応を書き起こした。
| |
文字列が、木になった。目で追える。枝の先に何があるか、たどれる。
「……待ってください」
私は画面から顔を上げた。10年の運用で染みついた勘が、何かを捉えていた。
「このオブジェクトツリーなら、interpret を呼ぶ前に木の構造を検査できる。括弧の閉じ忘れとか、存在しないフィールドの参照とか——デプロイ前に検出できる」
「気づいたかね、ワトソン君」
ロックさんが初めて笑った。不敵な笑みではなく、職人が同業者の発見を認めるような、控えめな表情だった。
「文法があれば、正しさは実行前に保証できる。それがevalとの決定的な差だ」
先月の3時間。括弧の閉じ忘れ。あれは文字列の中に閉じ込められていたから、デプロイして実行するまで誰も気づけなかった。構文木なら、組み立ての時点で型が合わなければエラーになる。
事件の終わり——リファクタリング後の CampaignEngine
ロックさんの指示に従って、CampaignEngine を書き換えた。
| |
eval が消えた。condition_expr というキーも、文字列も。代わりに condition キーに Expression ロールを消費するオブジェクトが入る。
| |
テストを書いた。TerminalExpression の単体テスト、NonterminalExpression の合成テスト、CampaignEngine の統合テスト。存在しないフィールドへのデフォルト挙動も確認した。
全テスト通過。
「テストが書ける。evalの文字列にはテストが書けなかった。10年間、祈りながらデプロイしていた」
ロックさんはペンをノートに挟み、立ち上がった。
「祈祷から工学へ。それがパターンの力だ」
「DBの200本のルール文字列はどうすればいいですか。全部書き直すんですか」
「パーサーを書きたまえ。文字列を読み取って構文木を生成する、正しい翻訳者だ。だが今日の仕事は文法の定義だ。翻訳者は、文法の上に立つ」
200本の移行は、一日の仕事ではない。しかし、文法が定義された今なら、パーサーを書くことは工学の問題であって呪文の問題ではない。
エピローグ
ロックさんが会議室のドアに手をかけた。
「ワトソン君、最後に一つ」
「なんですか」
「この文法に名前をつけておきたまえ。名前のない言語は、また呪文に戻る」
ドアが閉まった。空調のうなりだけが戻ってくる。
私は画面を見つめた。Expression ロール。Expr::And。Expr::GreaterThanOrEqual。10年間、文字列の中に閉じ込めていたものが、名前を持ったクラスとして並んでいる。
「……DiscountDSL、か」
10年越しの命名にしては、悪くない。
探偵の調査報告書
| 容疑(アンチパターン) | 真実(パターン) | 証拠(効果) |
|---|---|---|
| eval地獄(文字列ベースのルール実行) | Interpreter Pattern(文法のオブジェクトツリー化) | 構文エラーをデプロイ前に検出可能 |
| 文法なき言語(正規表現・文字列の乱用) | 抽象構文木(AST)による構造化 | 各ノードが責任範囲だけを評価し、テスト可能に |
| 設計の不在(暗黙の文法を人手で維持) | Expression Role + Terminal/Nonterminal分離 | 新ルール追加時にクラス追加のみ。既存コード変更不要 |
推理のステップ
- Expression ロール定義:
Moo::Roleでrequires 'interpret'を宣言し、すべての式が従う契約を定める - TerminalExpression 実装:
Expr::GreaterThan,Expr::GreaterThanOrEqual,Expr::Equalsなど、分解不能な原子的判定をクラス化 - NonterminalExpression 実装:
Expr::And,Expr::Or,Expr::Notで終端式を木構造として合成 - 構文木構築: eval文字列をオブジェクトツリーに変換し、Before/After の対応を確認
- CampaignEngine 統合:
condition_expr(文字列)をcondition(Expression オブジェクト)に置き換え、evalを除去 - テスト検証: 終端式・非終端式の単体テスト、複合構文木のネストテスト、統合テスト、エッジケースをすべて通過
ロックより
10年間の祈祷を責めるつもりはない。システムが動き続けたのは、君の技量があったからだ。 だが、技量に依存するシステムは、技量とともに失われる。文法をオブジェクトとして定義することは、君の知識をコードに翻訳する行為だ。 翻訳が終われば、君がいなくても文法は残る。——それが工学というものだよ、ワトソン君。
