第12章:5日間で、6回ひっくり返した

全体像の中でこの章が扱う部分: アプリごとの適応層(詳細は付録「全体像」を参照)

ここから第VI部です。まとめれば強くなる、とは限らない、という問いをこの部で扱います。第8章は、散らばった状態を一つのFSM(状態機械)にまとめ、その中身をさらに手続きと状態へ整理する話でした。第9章は、時間をまたいで届く観測をどう扱うかという話でした。どちらも、扱う範囲は一つのプロファイル、一つの大きな構造の内側に閉じていました。本章が扱う範囲はもう一段広がります。複数の異なるアプリと複数の異なるIME(日本語入力エンジン)にまたがるロジックを、同じものとして扱ってよいのか。空間をまたぐ同一視を、どこまで信じてよいのかという問いです。

第10章・第11章とも独立した柱です。あちらは、一つの試行が時間をまたいで別の試行と取り違えられる問題でした。本章は、複数の実行文脈(アプリ×IME)にまたがるロジックを、一つのものとして共有してよいかという、質的に異なる軸を扱います。答えは、思っていたよりも単純ではありませんでした。

正しいキーは、5日間で6回入れ替わった

docs/experiments.mdのエントリ01は、2026年6月27日から7月2日にかけて、あるキー選択が何度もひっくり返った様子を記録しています。

日付・コミット変更判明した事実
06-27 534051aVK_DBE_ALPHANUMERICへ切替即時OFFを達成、いったん採用
06-28 098c663F22へrevert直前の変更がフォーカス変更時に暴発していた
06-28 9c3f11e668a131VK_IME_OFFへ切替→即revert同日のうちに2回反転
06-28 adb856cVK_KANJIフォールバックへF22はTsfNativeで「半角英数」止まりと判明
06-28 b271aeeVK_IME_ON/OFFへ全廃config1.dbバインド不要・冪等と判断
07-01〜02 be3b056489cdf1MS-IME側も同じキーへ収束GjiDirect除外の撤廃で固着バグを解消

TSF(Text Services Framework)ネイティブなアプリ、たとえばWindows Terminalで、GJI(Google Japanese Input、Google 日本語入力)を直接入力(DirectInput)へ切り替えるには、どの仮想キーを送ればよいのか。候補はVK_KANJI(トグル式)・VK_DBE_ALPHANUMERIC(半角英数、ただしIMEはONのまま)・VK_IME_OFF(直接入力、冪等)・config1.db経由のF22キーの四つでしたが、単純な一つの答えはありませんでした。

表の最後の行は、GJI以外のIME(MS-IME)でも同じ収束が必要だったことを示しています。プロファイルという軸だけでなく、動作対象のIMEという軸もこの反転劇には絡んでおり、この二つの軸が絡み合っていたことこそが、本章が最終的にたどり着く発見の伏線です。

章のタイトルにある「6回」は、表が示す6月27日から7月2日までの5日間に起きた反転の数です。約5週間さかのぼった5月22日には前史(d4d9e27)もありましたが、VK_IME_ON/OFF双方向制御をChromeが受け付けずVK_KANJIとshadowチェックの組み合わせへ撤退した、この一件は「6回」の数えには含まれていません。

一つひとつの反転には、その場では正しい理由がありました。VK_DBE_ALPHANUMERICは即時OFFを達成しましたが、フォーカス変更時に無関係な副作用を起こしました。F22は副作用こそ起こしませんでしたが、TSFネイティブなアプリでは「半角英数」までしか進まず、真のIME OFFに届いていませんでした。「即座に効く」ことは「安全である」ことを意味せず、F22の遅延に見えた欠点は、実はフォーカス変更時の暴発を偶然抑えていました。この、一つの環境で正しかった答えが別の環境では通用しないという構造は、次に紹介する定数のインフレと共通の根を持っています。

定数は増え続け、無関係な機能が同じ変数を取り合った

IME OFFキーの反転と並んで、もう一つの症状がありました。Chrome向けのprobe最小待機時間です。CHROME_PROBE_MIN_MS(20ms)から始まったこの定数は、CHROME_PROBE_LONG_IDLE_MIN_MS(100ms→200ms)を経て、CHROME_PROBE_F2_GJI_IDLE_MIN_MS(350ms)へと、5週間のうちに4段階で釣り上がりました。それぞれの段には「実測最大180ms程度にマージン170msを足して350ms」というように、一応の実測根拠が添えられていました。場当たり的に決めた数字ではありません。それでも、条件フラグの組み合わせが増えるたびに、同じ目的の定数がprobe_fsm.rsという一つの共有分岐へ次々に追加され続けたという構造は変わりませんでした。本来はプロファイルごとに固有であるはずの待機ロジックが、プロファイルに依存しないはずの共有関数のパラメータとして表現されていたのです。

もう一つの実例は、共有可変状態の相互汚染です(BUG-23)。VcXsrv由来のstuck Ctrlキー対策として、PHYSICAL_KEY_STATEという共有テーブルにinjectedフィルタを追加した変更がありました。この変更が、まったく無関係なpanic_reset()の自己注入(send_all_modifier_key_ups())まで巻き込んで弾いてしまい、パニックからの回復機能そのものを止めていました。パニックからの回復は、他のあらゆる保護機構が失敗したときの最後の砦です。その最後の砦が、無関係な変更の巻き添えで沈黙していたことは、単なる一バグ以上の重みを持っていました。一つの共有テーブルを、物理キー追跡・stuck Ctrl対策・panic回復という三つの目的が、互いに調整のないまま読み書きしていたことが原因でした。

キー選択の反転も、定数のインフレも、共有状態の汚染も、それぞれ別のバグとして個別に報告され、個別に直されてきました。しかし三つを並べて眺めると、偶然の一致には見えなくなります。これらはcrates/awase-windowsのIME制御が採ってきた「単一の汎用ループにプロファイルごとの分岐を持たせる」という設計判断が生んだ、コストの実例カタログではないか。この疑いが、次の仮説につながります。

「共通化」を前提にしてきたこと自体を疑った

awaseのIME制御は、AppImeProfile(Standard/Imm32Unavailable/TsfNative)という区分に対して、drift correction・cold-start probe・literal detect・focus settleという一つの汎用ループを適用し、ループの内部でプロファイルごとに分岐する構造を一貫して採ってきました。これは事故ではなく、意図的な設計判断でした。AppImePolicyのコード冒頭には「アプリ差分はこの型に閉じ込め、reducer本体にif-elseを増やさない」という原則が明記されています。

ただし、この一歩は途中で止まっていました。AppImePolicyは、プロファイルごとに異なるデータ(focus_settle_msdefault_feedbackactuator_kind)を一つの構造体にまとめる、という段階までは進んでいました。けれども、そのデータを読み出すロジックの側は、依然として一つの共有関数に留まったままでした。データは分かれているのに、コードは共有されている。統一と分離のどちらの利点も、中途半端にしか得られていない状態です。

この状態を言葉にしたのは、Claude Fable 5との壁打ちでした。「フレームワークは既にデータの中でフォークしている。プロファイル別の定数、アプリ別のキー選択。コードだけ共有してデータが分岐している現状は、統一と分離の悪いとこ取りだ」という指摘です。この一言が、症状の背後にある仮説をはっきりした言葉に変えました。「共有ループ+プロファイル分岐」という設計判断そのものが、ここまで見てきた症状の波及元ではないか。

ただし、この仮説にすぐ飛びつくわけにはいきません。「重複させれば安全になる」という対抗仮説も、同じくらいもっともらしく聞こえます。共有ループを分離すれば、プロファイルごとの副作用は起こらなくなるはずだという見立ては、直感としては自然です。しかし直感だけで大規模な書き換えに踏み切れば、もし見立てが外れていたときの後戻りは大きくなります。共有か重複か、どちらが正しいかを直感や標語で決めるのではなく、実際のデータで確かめる。これが次に採った実験でした。

撤回の理由を書き残さなければ、同じ発見を何度もやり直す

反転が繰り返された最大の理由は、前回それを捨てた理由がコミット本文から辿れなかったことです。実際VK_DBE_ALPHANUMERICは複数回採用と撤回を繰り返し、そのたびに「これは半角英数であって直接入力ではない」という同じ事実を再発見していました。

この経緯から、IME制御に関わるrevertコミットの本文にはアプリ・IMEの状態・再現手順を必ず書くという実験ログ規約が生まれました(第14章)。決定の日付と対話の出所まで書き残す規律も同じ延長にあり、標語ではなく記録で確かめるという、この章を貫く態度そのものでした。

43件を、直感ではなく数えることにした

実装に着手する前に、まずdocs/known-bugs.mdに記録された既知バグ43件を全件読み、cross-profile spillover・single-profile・cross-cutting infraの3つに分類する。これがADR-081の第一歩(Phase 0)でした。「DRY」や「早すぎる抽象化を避けよ」といった標語で議論を打ち切るのではなく、「共有が実際に何パーセントの実害を生んでいるか」を実測で問う姿勢そのものが、この章でもっとも実践しやすい工夫です。この分類は「直感的にどちらの側に近いか」ではなく、docs/known-bugs.mdの各エントリの記述を読み、機械的に43件すべてへ当てはめて決められました。

分類件数比率意味
(a) cross-profile spillover11件26%あるプロファイル向け修正が別プロファイルに波及
(b) single-profile4件9%単一プロファイルに完全に閉じる
(c) cross-cutting infra28件65%プロファイル分岐と無関係な汎用インフラの欠陥

26%は「共有ループの分岐面」が実害を生んできた証拠であり、分離を進める根拠になります。この11件には、第10章で扱った柱Aの主題、BUG-43(「同じir_apply_drift_correctionが観測ストアを更新しないため無限に再送する」開ループ/閉ループの取り違え)も含まれていました。時間軸の問題として見たBUG-43は、空間軸で見れば「共有関数のnon-ImmCross分岐が3世代連続で再発した事例」でもあります。同じ一件のバグが、どの軸から光を当てるかで違う教訓を語ります。

一方、残りの65%はプロファイル分岐と無関係な、スレッド配送・タイマー・検出ヒューリスティックといった汎用インフラそのものの欠陥でした。ここはドライバをいくら分離しても、同じ形のバグが同じ場所に残るだけです。

「重複すれば安全」という仮説にも、反証があった

もっと重要な発見は、(c)の28件のうち6件でした。この6件は、既に部分的に分離されている実装同士が、互いの同期を怠って乖離したことが原因のリグレッションでした。TSF用とChrome用に分けて実装していた検出ロジックが、それぞれ別々に手が入るうちに食い違っていく。この6件のうち一件(BUG-30)は、分岐していた検出ロジックを一本化したことそのものが直接の修正になった、逆方向の実例でした。分離が原因でバグが生まれ、統一がそのバグを解消する。まさに「重複させれば安全」という側の対抗仮説を、データそのものが裏切っていました。

「共有すれば危険、重複すれば安全」という単純な対立軸そのものが、この6件によって崩れます。共有にも重複にも、それぞれ別の失敗モードでバグを生む余地がある。データが示していたのは、二項対立ではなく三すくみに近い構造でした。

分離の判断を実装より先に済ませるため、費用も机上で見積もりました。ImeOpenStrategyの4戦略は共有部分がtrait定義12行+走査ループ約25行のみとほぼ分離済みで、AppImePolicy(構造体17行+match35行+テスト約40行、計約92行)を独立したtrait実装へ完全分離した場合の純増は約60〜100行と見積もられました。行数という粗い尺度でも、重複のコストを感覚でなく数値で見積もるという手法自体が、この章の工夫の一つです。

結論は「全面Go」でも「全面No-Go」でもなく、限定的なGoでした。26%という比率は無視できずImeOpenStrategyは分離済みで移行コストが小さい一方、65%は分離では解決せず、6件の反証データは「重複すれば安全」という仮説も否定している。両方を踏まえ、最小の1プロファイルだけを試験実装してから見積もりを更新する、という段階的な進め方が選ばれました。

この限定的なGoには、もう一つの備えがありました。ADR-081は実装に着手する前に完了の判定基準も先に決めており、cross-profile spilloverの11件と分離済み実装がdriftした6件、合わせて約40%の再発率が下がったかどうかで判定します。総バグ件数だけを見れば分離と無関係な65%の変動に埋もれ、施策の効果を実装前から見誤ることを防いでいます。

答えは「共有か重複か」ではなかった

Phase 0で集めたデータを見つめ直すと、決定的な気づきがありました。「共有か重複か」という問いの立て方自体が、単純化しすぎていたのです。43件のデータは、どちらか一方を選べば解決するという二項対立ではなく、どちらの設計でも、別の失敗モードでバグが生まれることを裏付けていました。

問いを立て直すと、根本原因が見えてきます。awaseのIME制御は、二つの独立した軸を、一つの次元へ押し込んでいたのです。

一つは、プロファイル軸(静的)です。アプリがIMEの状態をどこまで観測・制御できるか(Standard/Imm32Unavailable/TsfNative)は、そのアプリが起動している間、変化しません。もう一つは、IME軸(動的)です。実行中にどのIME(GJIかMS-IMEか)がアクティブかは、ユーザーが切り替えれば実行時に変わります。この二つを同じmatch文の中で一緒に分岐させていたことが、症状の根にありました。

もし各プロファイル用のドライバへ単純に分離していたら、GJI固有の処理を三つのドライバがそれぞれ再実装することになっていたはずです。これはまさに、Phase 0で見つけた反証データ6件と同じ失敗モードの再生産でした。静的な軸と動的な軸を取り違えたまま分離を進めると、分離そのものが新しいバグの温床になります。

設計の候補は二つ検討されました。一つは動的な観測値をメソッド引数として各ドライバへ渡し、内部で分岐させる案。もう一つは、GJI直接制御を共有機構として一箇所に残し、各ドライバはuses_gji_direct() -> boolという静的な宣言だけで表す案です。採用されたのは後者でした。「profile軸(静的)とIME軸(動的)は本質的に直交する2次元だ。前者は各ドライバのメソッド内分岐として1次元に押し込む案であり、結果として3ドライバがそれぞれGJI分岐を再実装することになる。これはPhase 0で見つかった反証データ6件と同じ失敗モードの再生産である」と記録されています。

この章の冒頭に立てた問い、「共有か重複かは、何で決めるべきか」への答えは、「重複させるかどうか」という選択そのものではありませんでした。その変化がどの軸に属するかを、先に見極めることです。静的な軸は型で分離し重複を許容してよく、動的な軸は実行時に変わるため一箇所の共有機構に残す。IME OFFキーが5日間で6回ひっくり返っていたのは、この見極めをしないまま、静的な判断と動的な判断を同じ場所に書き込み続けていたからでした。

静的な軸は型に、動的な軸は一箇所に閉じ込めた

採用したのは、ImeProfileDriverという trait(複数の型に同じ振る舞いを約束させる、Rustの仕組み)を新設し、プロファイルごとに独立した構造体(ImmCrossDriverImm32UnavailableDriverTsfNativeDriver)を持たせる設計です。各ドライバは、自分のcold-start probe予算・drift correctionのfeedback方針・IME OFFキー選択・focus settle時間を、自分自身で所有します。プロファイル軸という静的な変化は、型として分離し、重複を許容します。

型を分けるだけでは、実際の開発は元に戻ってしまいがちです。複数のドライバにまたがる変更を書くたびに、共有ロジックへ逆戻りする誘惑が生まれるからです。この誘惑に歯止めをかけるため、ある変更が複数のドライバをまたぐ場合には、なぜまたぐ必要があるのかをコミットメッセージで説明する規約も、あわせて敷かれました。フォールバックの連鎖でどうにか帳尻を合わせるのではなく、各ドライバが自分の責務を自己完結させることを優先する、という考え方です。

trait ImeProfileDriver {
    fn uses_gji_direct(&self) -> bool;
    fn owns_physical_kanji(&self) -> bool;
    fn ime_off_key(&self) -> ImeOffKey;
}

一方、IME軸という動的な変化はGjiDirectMechanismという共有機構として一箇所にだけ実装しました。この機構への到達可能性は、非公開フィールドを持つcapability token(GjiDirectAccess)によって型として閉じられ、唯一の公開コンストラクタはuses_gji_direct()trueを返すドライバにしか値を渡しません。ImmCrossDriverのようなこの機構を必要としないドライバは、構造的にGJI機構を呼び出すコードを書くことすらできず、実行時チェックではなくそもそも書けなくする強制の仕方です。

二つの軸がそれぞれどこに置かれたかは、図にすると分かれ目がはっきりします。

プロファイル軸は型ごとに重複を許し、IME軸は一箇所の共有機構にcapability token越しでのみ到達させる

上段の3つのドライバは、それぞれが自分のcold-start予算・feedback方針・IME OFFキー選択を所有する、型で分離された静的な軸です。下段の共有機構は実行中に切り替わる動的な軸で、uses_gji_direct()trueを返すドライバからしか矢印が伸びていません。

移行は一度に切り替えず、段階を分けました。ADR-082のjournal配線を先行させるPhase 0.5から始まり、Imm32UnavailableDriverを切り出す1a、TsfNativeDriverを切り出す1b、5件の契約テストとレジストリを備える1cと進み、実機への配線1dと旧経路の撤去1eはまだこの先の計画です。全体を貫く方針は、分離を一括完了させてから旧経路をまとめて消すのではなく、1プロファイルのソークに合格するたびに、即座にその旧経路だけを撤去するという漸進性でした。

Phase 1cで実装された契約テストは5件です。IME-ON経路を持つドライバは、消えてしまった状態(ObservedEisu)を救済する経路を必ず対で持つこと。物理KANJIキーを自分の管轄だと宣言したドライバは、それを漏らさず処理すること。Blindのgive-up後には観測ストアへ何も書き込まないこと(第10章で見た観測の偽装、BUG-33型の再発防止)。beliefを高速にONへ倒す経路でも、必ずGjiFsmの同期を通ること(反証データ6件のうちもっとも実害が大きかった失敗モードへの対応)。そしてGJI機構の状態遷移は、どのドライバ経由でも同じGjiFsm同期を通ること。五つとも、Phase 0が実データから拾い出した失敗モードに、一対一で対応しています。この時点(Phase 1c)ではcargo test -p awase-windows --libが172件すべて通過しており、Windows実機やwine環境がなくても型設計と契約テストの正しさはLinux上で検証できることを、ドライバとcapability tokenを純粋なデータ構造として設計した効果として示しています。

分離の前後を整理すると、変化がどこに起きたかがはっきりします。

変更前変更後
AppImePolicy(構造体+match、約92行)ImeProfileDriver実装3種+共有機構1種
GJI直接制御(各所に暗黙のif分岐)GjiDirectMechanism+capability token1箇所
プロファイル差分(データのみ分離)プロファイル差分(データとロジックの両方を分離)
契約テストなし契約テスト5件(Phase 1c)

もっとも重要なのは3行目です。以前のAppImePolicyはデータだけを分けてロジックは共有したままの「Step 1.5」で止まっており、今回の分離はその中途半端さを解消する変更でした。

実機への配線(Phase 1d)と旧経路の撤去(Phase 1e)は、本書執筆時点でまだ着手されていません。移行の安全策として計画されているのはstrangler figパターンで、新旧の経路を並走させる期間中、実際に外界へ書き込む(actuateする)のは常に片方だけとし、もう片方は結果を比較するだけの読み取り専用shadowにとどめ、二つの実装が食い違ったときにどちらが真実か分からなくなる事故を構造的に排除します。ここまでの型設計と契約テストはWindows実機のいない環境でも検証できましたが、実際にキーが正しく届くかは、まだ確かめられていません。

軸を見誤れば、同じ失敗が形を変えて戻ってくる

この章の答えは、静的な軸を型で分離し、動的な軸を一箇所の共有機構に残す、という一つの判断基準でした。この基準が使えるのは、変化の軸が二つに分解でき、かつ直交している場合に限られ、見誤ればPhase 0が暴いた反証データと同じ失敗が形を変えて再発します。43件という数字もこの一つの手続き群(IME制御)における内訳にすぎず、Phase 1d・1eという移行のクライマックスも、まだ実機で確かめられていません。

型で分離するという答えそのものにも、後日談があります。2026年8月、「Rustの型システムをもっと生かせないか」という提案を受け、ImeProfileDriverとは別に、capabilityをtrait静的分岐で表す案と、観測プールをsealed trait(外部から新しい実装を追加できないように閉じたtrait)で2分割する案が検討されました(ADR-089)。しかしどちらも却下されています。理由は、traitが表現できるのはプロファイル軸のような静的な変化だけであり、実際には(プロファイル/IME種別)という2軸を同時に扱う必要があるため、traitという表現手段そのものが足りなかったからです。const表(caps(p, k)という関数)のほうが、traitより精緻にこの2軸を表現できると判断されました。この帰結として、この章が「まだ実機で確かめられていません」と書いたImeProfileDriverのPhase 1d(実配線)・1e(旧経路撤去)は、その後実機の有無とは無関係な理由で、正式に凍結されています(ADR-090)。型状態パターン自体が撤回されたわけではなく、実際に失敗を踏んだ3箇所――観測プールの排他・Actuationの型状態チェーン・GjiFsm同期義務――にだけ絞って投下し直されました。導入後の記録は、「型化の成果は本数が減ることではなく、同じ本数のガードがより広い範囲を守る形に置き換わることだった」と正直に振り返っています。軸を型で分離することと、その型をどう表現するか(traitか、それとも一段抽象度の低いconst表か)は、別の設計判断でした。

ここから得られる原則は、この章の「静的な軸は型に、動的な軸は一箇所に」とは別の一段です。型による抽象化は、それが表現できる軸の数までしか安全に効きません。実際に変化する軸の数が、選んだ型の表現力を超えていないかを、型を選ぶ前に数えておく必要があります。数え間違えれば、型を捨ててやり直すコストは、最初から数を数えるコストよりずっと高くつきます。

型で分離するという答えには、もう一段続きがあります。2026年8月、第11章で見た世代フェンシング(epoch fencing)の識別子――epochとウィンドウハンドル(hwnd、生成したウィンドウを指す番号)の束――の役割整理(ADR-106)で、観測を受理する仕組みと、actuationの着弾先が同一かどうかを確かめる仕組みを、同じ機構に統合してよいかが争点になりました。Opus(Claude Opusモデル)2体による敵対的レビュー(互いの設計を批判させ合うレビュー)は2ラウンドを経ても収束せず、著者は実機実験ではなくdocs/known-bugs.mdと既存ADRを掘り返す作業で決着させています。

過去の記録を数えると、別目的の値を1つの機構に共有したケースは、実際に2回バグを起こしていました。idle-conv-checkのスパムガード用フラグをresyncトリガーと共有し、無関係なキーが先にin-flight(処理が完了せず宙に浮いている状態)を掴んでresyncが早期終了した例と、effective_open()がengine内部の判断とOSへの書き込み許可という2つの役目を兼ね、1つのbool値(true/falseだけを持つ値)の反転が両方を同時に動かしてしまった前掲(第9章)の例です。一方、同じ形の型を複数箇所で別々のインスタンスとして使い回すこと自体には、失敗の記録がありませんでした。ここから「値を共有するな、型のパターンだけ共有せよ」という基準に到達しています。

失敗した2件と、成功した1件を並べると、分かれ目は「共有したもの」の違いにあります。

値を1つの機構に共有した2件は壊れ、型のパターンだけを複製した1件は壊れなかった

BUG-77とeffective_open()は、どちらも「1つの値」を2つの異なる目的に使い回したことが原因でした。一方ActuationTargetは、同じ形の型を、着弾先ごとに独立したインスタンスとして複製しています。共有したのは値ではなく、型という設計の型紙(パターン)だけでした。

ここから得られるのは、この章の「静的な軸は型に、動的な軸は一箇所に」よりもう一段先の判断です。分離した軸を表す型を複数の使用箇所へ配るとき、「同じ形に見えるから1つにまとめたい」誘惑と、「別の目的のために作られた値だから分離しておくべきだ」という慎重さのどちらを取るかは、実機で挙動を試しても決まりません。過去に同じ統合を試みて実際に壊れた記録があるかどうかを、先に数えるほうが近道でした。なお、この統合の可否そのもの(決定5)は、着手条件が整わずまだ実装されていません。

他分野への転用

「静的な軸は型で分離し、動的な軸は一箇所に閉じ込める」という発想には、他の分野にも同じ形の実例があります。Webサービスのアクセス制御でよく使われるロールベースアクセス制御(RBAC)は、ユーザーの役割(管理者か一般ユーザーか)という起動時に決まって滅多に変わらない静的な属性と、そのユーザーがいまログイン中かどうかというセッション状態という動的な属性を、意図的に別の仕組みとして扱います。役割は権限テーブルという型に近い構造で管理し、セッションは有効期限つきの一箇所の共有ストアで管理する。両者を一つのテーブルへ一緒くたに書き込むと、本章の反証データ6件と同じ形の同期漏れが起こります。サブスクリプション型のSaaSでプラン種別とセッション状態を分けて管理する設計も、同じ形の分離です。

マイクロサービスの移行パターンとして知られるstrangler figは、新旧の実装を並走させながら、実際に書き込む権限だけは常に一方に絞ります。本章の移行計画は、この分野で繰り返し発見されてきた安全策の一つの適用例です。43件のバグを定量分類してから決めるという手法も同様で、「感覚ではなく実測で決める」という態度は、ソフトウェア開発に限らず再現しやすい工夫です。


設計原則

原則:「共有か重複か」は、直感ではなく既存のバグをコストとして数えてから決めます。

適用条件: 複数の実行文脈にまたがるロジックを、共有すべきか分離すべきか判断に迷ったときに働きます。「DRY」や「早すぎる抽象化を避けよ」といった標語だけでは、どちらの立場からも都合よく引用できてしまい、議論が収束しません。

実装の形: まず既存のバグ台帳を全件読み、症状がどの軸に起因するかを分類します。次に、変化の軸が複数あるなら、それぞれが静的(実行中に変わらない)か動的(実行時に変わる)かを見極めます。静的な軸は型やtraitの実装として分離してよく、動的な軸は一箇所の共有機構にまとめ、capability tokenのような型でアクセスを制限します。分離にかかる費用は、実装に着手する前に、型シグネチャの行数程度の粗さで見積もっておくと、途中で後戻りする可能性を減らせます。

限界: この原則はバグ台帳が十分に蓄積されている場合にしか使えません。新規に書き始めるコードには適用できず、まず動くものを作ってから、実際に踏んだ失敗を数えるという順序が必要です。また、変化の軸が二つより多く、互いに直交していない場合には、この基準だけでは割り切れません。