開発mdx-playground

前日の夕方に公開した state-vs-events.vue の記事に、夜のうちに反論コメントが3本届いた。「イベントソーシングの一言で済む話」「理想論でしかない」「体育館の人数管理ならステータスで数えるべき」。朝、ベッドの中でその3本を読み返したとき、頬が一瞬熱くなった。書き手として腹が立ったのではなく、3本ともに芯を喰っていたからだ。反論を本文に取り込んで書き直すしかない、と起きながら決めた。

きょうの作業は、ひとことで言えば「コメントを本文に編み込む」だった。一行で書ける主張をひっくり返されたら、ひっくり返した相手の言葉を地の文に混ぜて、もう一回立たせる。コードを書く以上に、文章を直す手数のほうが多かった一日だった。

反論1: 「イベントソーシングの一言で済む」を取り込む

最初の反論は、いちばん切れ味が鋭かった。「お前が長々と書いた話は、要するにイベントソーシング/CQRSだろう。一語で済む」と。

これは反論として正しい。ただ、記事の中でイベントソーシングという用語に触れていなかったのは、書き手としては手抜きだった。読者にとっては「知っているなら最初からそう書け」となる。

そこで本文に章を増やし、Martin Fowler の Event Sourcing 解説と Microsoft Azure Architecture Center のドキュメントへの言及を埋めた。「私の言葉で延々と説明したことは、業界ではすでにイベントソーシングと CQRS という名前がついている」と書き、用語の出典で殴られないように先回りした。

Claude Code に「Fowler の event sourcing と CQRS のページを根拠として段落を起こして、用語の初出に必ず一文の定義を添えて」と指示して書かせた。書かせたあと、こちらで「読み返し用の橋渡しの一文」を冒頭に足した。

反論2: 「理想論でしかない」を「現実的な境界線」で受け止める

二つ目の反論は、感情のこもったやつだった。「全部イベントで持つなんて、現場のコストを知らない人間の理想論だ」と。

これも、その通りだった。元の記事は「テーブルに状態を持たせるな」と断言する勢いで書いていて、断言の代償として現場の重さを書いていなかった。

そこで「現実的な境界線」という章を増やした。書いたことは3つ。

  • 規模が小さく、更新頻度も少ないなら、状態テーブル直書きでよい
  • ログを永久保持するつもりがないなら、イベントテーブルにも保持期間を切ってよい
  • どこで方式を切り替えるかは、ドメインの不可逆性と監査要求の強さで決める

ここで会計の話に橋を渡した。会計には「期末締め」と「翌期繰越仕訳」という発想があり、過去の仕訳を全部頭から辿り直さないで、期首残高というスナップショットから始める。これはイベントソーシングのスナップショット最適化と同じ構図だ、と一段落で書いた。会計士フォロワーが過半数を占める読者層に「自分の世界の言葉と同じだ」と感じてもらうための橋だ。

反論3: 「体育館の人数管理ならステータスで数えるべき」をデモで答える

三つ目の反論は、もっとも反応に困った。「お前が言うイベントソーシングは大げさだ。体育館の入退館人数を数えるのに、入館/退館のイベントを全部保持するなんて狂気の沙汰だ。今いる人数を整数で1つ持てばいい」と。

これは半分正しく、半分が落ちていた。「今いる人数」だけがほしいのか、「今日の入退館の履歴」も後で必要になるのか、で答えが変わる。

文章で説明するより、画面で見比べたほうが速い。そう思って、体育館の人数管理を題材にしたインタラクティブデモを追加した。3列でならべる。

  • 方式A: カウンタだけ(gym.current_count を increment/decrement)
  • 方式B: イベントだけ(gym_event テーブルに enter / leave を追記し、count は集計で導出)
  • 方式C: ハイブリッド(イベントを追記しつつ、現在人数のスナップショットも持つ)

3列のボタンを押すと、同じ操作(5人入る、2人出る、1人入る)が3方式に同時に流れて、内部テーブルがどう変わるかを左右に並べて見せる。「今いる人数を見たいだけならAで足りる」「監査で『18時の入館者を全部出して』と言われたらAは詰む」ということが、画面を見れば1秒で伝わる。

ロジックは useGymStory というコンポーザブルに切り出して、Claude Code に純粋関数として実装させた。テストは15件、ぜんぶグリーン。

// app/composables/useGymStory.ts(抜粋)
export const applyGymStep = (state: GymState, step: GymStep): GymState => {
  const eventA = step.kind === 'enter' ? +step.count : -step.count
  return {
    counter: state.counter + eventA,
    events: [...state.events, step],
    snapshot: state.snapshot + eventA,
  }
}

「次へ」方式のステップ送りで読み手の脳を待たせる

前日の state-vs-events.vue には「ストーリー再生」ボタンがあった。押すと一気に履歴が流れて、見終わったときには「結局何が起きた?」となる作りだった。これも反論コメントで「速すぎる」と言われた。

今日は「次へ」ボタンに変えて、1ステップずつ進む方式にした。読み手のクリックが、説明文の進行と同期する。アニメーションを派手にするより、こちらが手を止めるほうが、読み手の脳は追いつく。

ロジックは useMembershipStory というコンポーザブルに切り出して、テストを6件足した。「next() を呼ぶとカーソルが1進む」「終端で next() を呼んでも進まない」など、純粋関数だけで検証できる範囲に閉じた。

方式間の比較を公平にするための地味な修正

方式B/Cのデモには membership_event テーブルしかなくて、誰の入退会なのかが見えていなかった。これも反論コメントで「方式Aには member テーブルがあるのに、B/Cには無いのは不公平だ」と指摘された。

そこで方式B/Cのデモにも member テーブル(id, name)を追加して、membership_eventmember_id を生やした。外部キーの矢印を画面に描いて、方式A/B/Cが同じ「会員」概念を共有していることを目で確認できるようにした。地味だが、ここを揃えないと「方式Bは情報が足りない」という誤解で読まれて終わる。

複式簿記アナロジーを補足ボックスに昇格させる

きょうの作業で、いちばん書いていて手応えがあったのが、複式簿記アナロジーの強調だった。

会計士の方へ: 仕訳帳と残高試算表の関係を思い出してほしい。仕訳帳は追記専用ログ(=イベント)、残高試算表は時点の状態(=導出値)だ。残高だけ書き換えて仕訳を残さない会計帳簿は存在しない。残高は年齢に近く、仕訳は生年月日に近い。年齢は毎年書き換わる導出値、生年月日は一度確定すれば二度と変わらない事実。テーブル設計でも、書き換える数字と、保存すべき事実は分けたほうがよい。

これを記事の中盤に「会計士向けの補足ボックス」として独立させた。文章の真ん中で読者層が分岐するときは、地の文に混ぜるより、囲みで「ここから先は会計の話」と宣言したほうが、読み手の負荷が軽くなる。

「ドメインの言葉に論理削除はない」という既存セクションの末尾には、「これは方式Bを支持する」と一文だけ足した。書き手として方針を曖昧にしたまま終わらせない、という押し付けがましさを残した。

タイトルから断言を抜く

朝の段階で、タイトルも変えた。

  • 前: 「テーブルに状態を持たせるな」
  • 後: 「テーブルに状態を持たせてはいけない(は本当か)――3つの設計を見比べる」

断言を残した上で「は本当か」を括弧で挟む形にした。SEO的には長くなったが、訂正記事だと冒頭で宣言したほうが、前日の記事のリンクを踏んできた読者に対して誠実だ。リード文も「テーブルに状態を持たせる設計は間違いだ」から「テーブルに状態を持たせる設計はケースバイケースだ。前日の記事を書き直す」に変えた。

左ボーダーを全廃して、スキルに恒久ルールを追記

文章とは別に、CSSの掃除もした。前日の記事には、引用や注釈に左ボーダー(縦線の色付きアクセント)を多用していた。今日の作業で並べてみたら、強調が過剰で「ここが大事、ここも大事、ここも大事」と全部叫んでいる状態だった。記事から左ボーダーを全部抜いて、background-color の薄い差で区別する方式にそろえた。

ついでに vue-pages スキルに「左ボーダー(border-left のアクセント)禁止」を恒久ルールとして書き加えた。.claude/skills/vue-pages/SKILL.md.agents/skills/vue-pages/SKILL.md の両方に同じ文言を入れて同期させた。今後の自分が同じ過ちを繰り返さないための保険だ。

試行錯誤の構図

きょう一日の構図を一行で書くと、こうだった。

  • 書いた記事に反論が来る
  • 反論を本文に取り込む
  • さらに反論が来る
  • デモを追加する
  • もう一回、反論を取り込む

反論は最初の段階では「攻撃」に見えるが、本文に編み込んだあとで読み返すと「補強の鉄筋」に変わる。書き手が一人で書いた記事よりも、反論を3本くぐらせた記事のほうが、構造が太くなる。

検証の数字

  • ユニットテスト全43件 green(useMembershipStory 6件 + useGymStory 15件 + 既存22件)
  • dev サーバ起動、/state-vs-events-revision を 200 で確認、コンソールエラーなし
  • カバレッジ: 関数 100%、行 90% 超
  • E2E は追加せず(純粋関数中心の修正のため)

振り返り

書いた記事に反論が来たとき、最初の反応で「無視するか」「取り込むか」の分岐が訪れる。今日のケースでは、3本ともに具体例(イベントソーシング、現場コスト、体育館人数)が乗っていたから、無視はできなかった。具体例を持って反論してくる読者がいることの幸運を、本文に編み込むことで返した。

会計の世界では「期末締めをして翌期繰越仕訳を切る」のが当たり前で、過去の仕訳を全部消すことは決してない。テーブル設計の話を会計のメタファーで説明できると確信したのが、今日の収穫だった。

明日やること

  • note の下書きに同じ記事を流し込んで、文中リンクの整形だけ手で直す
  • useMembershipStoryuseGymStory のロジックを app/utils/storyEngine.ts に共通化できるか検討する
  • 反論コメントの引用元(X の投稿)に「本文に取り込みました」とリプライする