「コード探偵ロックの事件簿」は、困ったコードを抱えた依頼人が、雑居ビルの一室にある「レガシー・コード・インベスティゲーション(LCI)」に駆け込む連載です。自称コード探偵のロックが、依頼人を勝手に「ワトソン君」と呼びながら、コードに残った証拠から原因を割り出し、設計の技法を使って直します。
全72話で、GoF のデザインパターンから、リファクタリングの技法、DDD(ドメイン駆動設計)、分散システムの障害対策までをたどります。どの話からでも読めます。実装は Perl と Moo ですが、直す前と後のコードを見比べて、何が変わったかを追えれば十分です。
このページは目次を兼ねた案内板です。初めてなら「まず読むなら」の3本から、手元に困っているコードがあるなら「困りごとから探す」から、順に通して読むなら「全話一覧」から選んでください。
まず読むなら
- Strategy — 形式を足すたびに巨大な if-elsif へ書き足している回。連載の第1話で、ほかの話を読む足場になる
- Dependency Injection — ローカルでは通るテストが CI で全部落ちる回。クラスの中の new を外へ出すだけで、明日からのテストが書きやすくなる
- Circuit Breaker — 外部 API の障害でリトライを増やすほど停止が長引く回。ここから5話、同じ依頼人の現場で障害対策を1つずつ積み上げていく
困りごとから探す
条件分岐が伸び続けて、足すたびにどこかが壊れる
- Strategy — 出力形式ごとに分岐する5000行のメソッド
- State — 0〜3の数字で持つ注文の状態
- State — ステータスを足したら無関係な処理が壊れる状態遷移
- Chain of Responsibility — 認証や権限チェックを詰め込んだ if のネスト
- Composite — 「タスクかグループか」を全関数で判定している
- Visitor — 型判定の分岐を1か所書き忘れた
- Double Dispatch — 帳票と出力形式の組み合わせで書き忘れた
- Specification — 同じ条件が12か所の if に散らばっている
- Plugin — チャネルを足すたびにコアの if-elsif を書き換える
組み合わせの数だけ、クラスやコピペが増える
- Template Method — 手順が同じ処理を丸ごとコピーしている
- Decorator — 機能の組み合わせごとに継承でクラスを作っている
- Decorator — 通知のサブクラスが30個を超えた
- Bridge — 文書の種類×出力形式でクラスが掛け算に増える
- Prototype — 似た帳票を毎回ゼロから組み立てている
- Abstract Factory — セットで使う部品の組み合わせが混ざる
- Type Object — 値が違うだけのサブクラスが並んでいる
1か所直すと、関係ないはずの所が壊れる
- Observer — 1つのメソッドが通知もログも画面更新も呼んでいる
- Domain Event — 処理を足したら、別の処理が二重に走った
- Facade — 1つのクラスが全サブシステムを直接呼んでいる
- Mediator — モジュール同士が直接呼び合っている
- Proxy — 他チームのクラスの内部データを直接読んでいる
- Law of Demeter — メソッドチェーンの奥でカラム名の変更に巻き込まれた
- Feature Envy — 自分のデータを使わず、他クラスのデータばかり触っている
- Tell, Don’t Ask — 計算をすべて Service クラスが抱えている
- CQRS — 書き込みを直すと集計が落ち、集計を直すと書き込みが落ちる
- Bounded Context — 部門ごとに意味の違う顧客を1つのモデルにまとめた
- Pipeline — 全処理が500行のメソッドに詰まっている
- Iterator — 内部構造をたどるスクリプトが構造の変更で全滅した
値の取り違えやチェック漏れで、本番が落ちる
- Parameter Object — 引数の順番を取り違えた
- Builder — 12個の引数を順番で渡していて1個ずれた
- Builder — 決まった順でメソッドを呼ばないと動かない
- Value Object — JSON に出すと数値と文字列が入れ替わる
- Magic Numbers / Strings — 文字列の1文字のタイポ
- Money Pattern — 金額の丸め誤差と通貨の取り違え
- Entity vs Value Object — 等しいはずのオブジェクトが等しくならない
- Immutable Object — 渡したオブジェクトを渡した先で書き換えられた
- Null Object — undef のチェックを1か所書き忘れた
- Interpreter — 文字列のルールを eval で実行している
テストが遅い、CI で落ちる、差し替えられない
- Repository — DB が要るテストに30分かかる
- Singleton/DI — どこからでも呼べる DB 接続のせいでテストが本番を触った
- Service Locator — テストどうしが状態を共有して、毎日数件落ちる
- Dependency Injection — クラスの中で部品を new している
- Registry — プラグインを足すたびにコンストラクタとテストを直す
- Factory Method — 通知手段を足すたびに本体の new を書き換える
データが食い違う、元に戻せない
- Command — DB は戻せても外部 API の操作は戻せない
- Memento — 上書きした状態を前に戻せない
- Unit of Work — 複数のテーブルへの書き込みが途中で止まる
- Aggregate — 合計金額と明細が合わない
- Event Sourcing — 過去の時点の数が分からない
- Saga — サービスをまたぐ処理を途中で取り消せない
- Outbox — DB は更新できたのに通知が消えた
外部サービスの障害や負荷で、全体が止まる
- Publish-Subscribe — 1つのサービスが落ちると呼び出し元も止まる
- Circuit Breaker — リトライを増やすほど停止が長引く
- Retry Pattern — 全台が同時にリトライして短い障害が長引く
- Timeout — タイムアウトを既定値のまま放っている
- Fallback Pattern — 代わりの経路が同じ障害の原因を叩いている
- Rate Limiting — 通常の10倍のリクエストが来た
- Bulkhead — 1つの機能の暴走でサイト全体が止まった
- Health Check — ヘルスチェックは緑なのに画面が落ちている
- Dead Letter Queue — 処理できないメッセージがキューを塞いでいる
- Competing Consumers — 処理する台数を増やしたら二重に処理された
重い、メモリが足りない
- Flyweight — 同じデータを件数分コピーしている
- Object Pool — リクエストごとに DB 接続を作って捨てている
- Lazy Loading — 起動時に使わないデータまで全部読んでいる
古いシステムや外部の仕組みとつなぐ、置き換える
- Adapter — 新旧システムでメソッド名もデータの形も違う
- Anti-Corruption Layer — 外部 API の項目名が社内のコードに入り込んでいる
- Strangler Fig — 古いシステムを一斉に切り替えようとして失敗した
- API Gateway — フロントがバックエンドの URL を全部知っている
- Backends for Frontends — Web とモバイルで同じ API を使っている
消せない、合わない、意味のないコードが残っている
- Dead Code / Lava Flow — 「消すと壊れるかも」で残したコード
- Refused Bequest — 継承したのに使えないメソッドを die で潰している
- Middle Man — 転送するだけのクラスが挟まっている
全話一覧
デザインパターンと設計の基本(第1〜27話)
- 形式を足すたび5000行のif-elsifに書き足す。形式ごとのクラスに分ける【Strategy】
- CSV出力のバグを直すと、コピペした隣にも同じ修正が要る。手順を親にまとめる【Template Method】
- 在庫更新を1か所直すと、メール通知やダッシュボードが止まる。通知で受け手を分ける【Observer】
- ポイント付与の機能を継承で組み合わせたら、機能3つで8クラス。外から重ねて足す【Decorator】
- 0〜3の数字で持つ注文状態、発送済みにキャンセルが通った。状態をクラスにする【State】
- JSONで年齢が文字列になり、電話番号の先頭の0が消える。値を専用のクラスで包む【Value Object】
- 割引ルールを足すたび数千行のServiceを直す。計算をデータの側へ戻す【Tell, Don’t Ask】
- 夜間バッチを並列にしたら、テストの処理が本番DBを書き換えた。接続を外から渡す【Singleton/DI】
- 引数10個の関数が5階層、IDの順番を取り違えた。引数を1つにまとめる【Parameter Object】
- 1クラスが全サブシステムを直接呼び、仕様変更1つでエラーが連鎖。窓口を立てる【Facade】
- 失敗してDBは戻せても、決済APIの課金は取り消せない。操作に取り消しを持たせる【Command】
- トークンの期限を変えただけで、全APIが認証エラーで落ちた。チェックを鎖に分ける【Chain of Responsibility】
- 他チームのクラスの内部を直接読み、キー名の変更でAPIが全滅。代理人越しに触る【Proxy】
- 通知手段を足すたびに、決済ロジックの中のnewを書き換える。生成を工場に移す【Factory Method】
- ステータスを1つ足したら、無関係な出荷処理でバグが続発。状態ごとにクラスを分ける【State】
- 新旧システムでメソッド名もデータの形も違い、if分岐が溜まる。変換役を挟む【Adapter】
- 引数12個のコンストラクタで1個ずれ、10万円の本番事故。名前を付けて組み立てる【Builder】
- 帳票に項目を1つ足したら、17種類の出力が次々に壊れた。原型を複製して作る【Prototype】
- 通知のサブクラスが30個超、SMSを足せばまた増える。機能を重ね着で組み合わせる【Decorator】
- サブタスクを入れたら、階層が深いと進捗率の計算がバグる。個と集まりを同じに扱う【Composite】
- 5つのモジュールが互いを直接呼び、どこを触っても他が壊れる。仲介役に集める【Mediator】
- 砂漠にクラーケン、海底にサボテン。地形と生物の組み合わせが混ざる。セットで作る【Abstract Factory】
- コピペで作った提案書に、請求書の消費税欄が出た。文書の種類と出力形式を分ける【Bridge】
- 10万件の商品をメモリに載せたら2.4GBで落ちた。共通データを共有して減らす【Flyweight】
- 条項の削除を戻せず、免責条項の抜けた契約書が締結された。操作前の状態を残す【Memento】
- ツリーの構造を変えたら、15部署のスクリプトが全滅した。たどり方だけを公開する【Iterator】
- 種別を足して分岐を1か所書き忘れ、未受講の200名に修了証が出た。漏れを防ぐ【Visitor】
コードのにおいと依存の扱い(第28〜46話)
- DBが要るテストに30分、CIが毎回タイムアウトする。データアクセスを分ける【Repository】
- definedを1か所書き忘れ、深夜3時に通知が落ちた。何もしない実装を置く【Null Object】
- 監査で30日前の在庫を聞かれても、上書きした在庫数からは出せない。出来事を残す【Event Sourcing】
- 受注チェックを直すと集計テストが落ち、集計を直すと受注が落ちる。読み書きを分ける【CQRS】
- 注文はあるのに在庫が減っていない。毎週手で直す不整合を、変更を一括で確定して防ぐ【Unit of Work】
- 機能を足すたび無関係なテストが壊れる。他クラスのデータを触るメソッドを持ち主へ【Feature Envy】
- 住所のカラム名を変えたら、触っていない配送料と税の計算が落ちた。鎖を断つ【Law of Demeter】
- メソッドチェーンを委譲で潰したら、転送するだけのクラスができた。仲介役を外す【Middle Man】
- 継承したのに使えないメソッドをdieで潰している。継承をやめて必要な能力だけ持つ【Refused Bequest】
- 「消すと壊れるかも」で残したコードが積もり、誰も触れない。根拠を持って消す【Dead Code / Lava Flow】
- ‘actve’、1文字のタイポで深夜に2時間。文字列と数値に名前と型を付ける【Magic Numbers / Strings】
- initとconfigureの後でないとstartが動かない。半端な状態を作らせない【Builder】
- テストコードは触っていないのに、夜間テストが毎日数件ずつ赤くなる。依存を明示する【Service Locator】
- ローカルでは通るテストが、CIでは全部赤。クラスの中のnewを外から渡す【Dependency Injection】
- プラグインを1つ足すたびにコンストラクタとテストを全部直す。棚に登録する形にする【Registry】
- 閾値を変えたら12か所のifのうち3か所を直し漏れた。条件を1つの部品にまとめる【Specification】
- 商品カテゴリが増えるたびにサブクラスを足し、次は20個追加。種類をデータで表す【Type Object】
- Webhookを足したらSlack通知が壊れた。チャネルを差し込み式に分ける【Plugin】
- ルール文字列をevalで実行、括弧の閉じ忘れで3時間の本番停止。文法を組み立てる【Interpreter】
ドメインのモデリング(第47〜54話)
- 月末のたびに注文の合計金額と明細が合わず、SQLで手直しする。注文を門番にする【Aggregate】
- ポイント加算を足したら、在庫引き当てが二重に走った。起きたことを通知で伝える【Domain Event】
- 顧客マスタを1つにしたら、営業のメール変更で配送通知の宛先まで変わった。境界を引く【Bounded Context】
- 同じ住所なのに等しくない、同じ物件なのに更新後は別物。比べ方を2つに分ける【Entity vs Value Object】
- 渡した料金設定を別チームが書き換え、誤請求が出た。書き換えられない形にする【Immutable Object】
- 請求金額が1円ずれ、ドル建てに円の手数料が足された。金額と通貨を一緒に持つ【Money Pattern】
- 500行のメソッドは1行直すと無関係な所が壊れ、テストも書けない。段に分ける【Pipeline】
- 帳票の種類と出力形式の組み合わせで、CSVだけ分岐を書き忘れた。漏れを型で防ぐ【Double Dispatch】
分散システムと障害対策(第55〜72話)
- 決済のタイムアウトで在庫と配送も止まり、300件を手で戻した。手順ごとに取り消す【Saga】
- DBに書けたのに通知が消え、在庫だけ確保された注文が残る。送る内容もDBに書く【Outbox】
- 請求サービスが落ちるたびに注文確定が止まる。直接呼ばずに掲示板に貼る【Publish-Subscribe】
- 決済APIの障害でリトライを増やすほど、停止が長引いた。呼び出しを遮断する【Circuit Breaker】
- 10秒の障害が、全台の一斉リトライで60秒の停止に。待ち時間をずらして防ぐ【Retry Pattern】
- CBもリトライも正常なのに、在庫APIの遅延で注文が30分止まった。上限を決める【Timeout】
- フォールバックが同じDBを叩き、30秒のはずの停止が30分に。別の経路を用意する【Fallback Pattern】
- 通常の10倍のリクエストが来て、注文APIが止まった。受け付ける量に上限を付ける【Rate Limiting】
- 月末のバッチがDB接続を使い切り、サイト全体が30分止まった。区画を分けて守る【Bulkhead】
- リクエストごとにDB接続を作って捨て、p99が350msに。接続を使い回す【Object Pool】
- 起動時に全件を先読みし、起動に42秒、メモリは1.3GB超。必要になってから読む【Lazy Loading】
- /healthは緑なのに画面が500を返した。生存と準備完了を分けて答える【Health Check】
- 処理できないメッセージが戻り続け、メールの送信キューが7時間止まった。隔離する【Dead Letter Queue】
- キューの処理を4台に増やしたら、同じ出荷が二重に予約された。重複を1回に抑える【Competing Consumers】
- 外部APIの値が1つ増えるだけで、社内のPRが7本立つ。外の言葉を境界で訳す【Anti-Corruption Layer】
- 一斉切り替えの演習で、未実装のキャンセルまで新系へ送られた。機能ごとに移す【Strangler Fig】
- バックエンドのURLが3か月で6回変わり、フロントを毎回直す。窓口を1つにする【API Gateway】
- Web向けAPIをモバイルでも使い、重いうえに足りない。窓口を端末ごとに分ける【Backends for Frontends】
