あの巨大レガシー、どう倒す?海外C#エンジニアが実践した「ストラングラーフィグ」でWPFモノリスから生還した話

「触るな危険」のレッドゾーン。巨大モノリスと僕らの終わらない残業

どうも!ヒロです。いま僕はヨーロッパのとある国で、C#とWPFをメインに使うITエンジニアとして働いています。こっちに来て早数年、コード書いて、ミーティングして、時差ボケのインドチームとチャットして、まぁなんとかサバイブしてる毎日です。

海外で働くエンジニアって言うと、なんかキラキラしたイメージありますか?最新の技術スタックで、スマートな多国籍チームと、アジャイルにスプリント回して…みたいな。

うん、そういう側面もゼロじゃない。でもね、現実はそんな甘くない。

僕ら「海外出稼ぎエンジニア」の前に立ちはだかる最大の敵。それは、**「巨大モノリス・レガシーシステム」**です。

特に僕の主戦場であるWPF。Windowsのデスクトップアプリですね。これがまぁ、曲者(くせもの)なんですよ。金融、製造、物流…いろんな業界の基幹業務で、10年、15年と使われ続けてきた「秘伝のタレ」みたいなアプリケーションがゴロゴロしてる。

僕が今ジョインしてるプロジェクトも、まさにそれ。とある製造業の基幹システムで、設計から製造ラインの管理、在庫チェックまで、ぜーんぶ入りの「全部乗せラーメン」みたいなWPFアプリです。コード行数は…もう誰も数えたくないレベル。

これが、ヤバい。

何がヤバいって、もう「密結合」とかいうレベルじゃない。「融合」してる。

金曜の午後4時。週末前の穏やかな時間。

「ヒロ、ちょっといいか?顧客Aから、納品書画面のフッターに電話番号を追加してほしいって急ぎで頼まれたんだ。今日中にいけるか?」

マネージャーのマークが人の良さそうな顔で言ってくる。

(お、簡単じゃん。XAML(ザムル:WPFの画面定義ファイル)いじって、ViewModelからデータバインドするだけっしょ)

僕は余裕しゃくしゃくで引き受けた。

「OK、マーク。1時間もあれば終わるよ」

…それが、地獄の始まりでした。

まず、ビルドが通らない。いや、正確にはビルドに25分かかる。ちょっと修正して動作確認するたびに、コーヒー休憩が必要になるレベル。

やっとビルドが通って起動。よし、納品書画面を開くぞ…

「Unhandled exception has occurred in your application.」

(ハンドルされてない例外が発生したよ)

出たよ。おなじみのエラーダイアログ。

スタックトレース(エラーの履歴)を追うと、なぜか全然関係ないはずの「在庫管理モジュール」の奥深くでNullポインタ例外(ぬるぽ)が出てる。

なんで!?僕は納品書の「見た目」を触っただけだぞ!?

慌ててコードを追う。すると、納品書画面のベースクラス(親玉みたいなクラス)が、なぜか在庫管理モジュールが内部で使ってる共通ユーティリティクラスを「こっそり」継承していて、そのユーティリティクラスのコンストラクタ(初期化処理)が、今回の僕の修正で意図せず変更されたグローバルな静的変数(Shared Global Variable)を参照して、特定の条件下でNullを返すようになっていた…。

…意味わかります?(笑)

わかんないですよね。僕も書いてて意味わかんない。

でも、これがモノリスの日常なんです。

「あっちを直せば、こっちが壊れる」

「この修正の影響範囲?…神(と、5年前に辞めたロシア人の元エースエンジニア)のみぞ知る」

僕のチームには、シニアエンジニアのピーター(イギリス人)がいます。彼はこのシステムの生き字引みたいな人。

「ピーター、助けて。納品書いじったら在庫管理が死んだ」

「(ふかーいため息)…ああ、またか。ヒロ、それは『悪魔のグローバル変数』の仕業だ。たぶん、SharedConstants.cs の3800行目あたりを見てみろ。そこを false から true に…いや待て、それやると今度は会計モジュールがバグるんだった。えーと…」

もう、カオス。

新しい機能を追加するなんて夢のまた夢。ビジネスサイドからは「競合他社はもうWebで同じことやってるぞ!ウチも早くクラウド対応しろ!」って言われる。

こっちは「無理言うな!このWPFアプリ、ちょっと触っただけで会社が止まるんだぞ!」って言いたい。

これが「技術的負債」ってやつです。

しかも、海外の現場は人の入れ替わりが激しい。ドキュメントは基本、無いか、あっても5年前のバージョン。コードが唯一の真実(Code is the documentation)。

そんな「詰んだ」状況で、みんなどうしてると思います?

たいていのエンジニアは諦めます。

「これは俺の仕事じゃない。俺は言われたバグを直すだけ」

「どうせリプレイスするんでしょ?(永遠に来ないその日を待つ)」

マネージャーも諦めます。

「わかってる。わかってるんだ。だがビジネスを止めるわけにはいかない。頼む、なんとかパッチ(応急処置)を当ててくれ」

そして、みんなで「触るな危険」のレッドゾーンを避けながら、お祈りしながらデプロイする日々。

僕も、正直諦めかけてました。

(C# WPFのキャリア、もう終わりかな…。こんなレガシー触ってたら、市場価値下がる一方だ…)

でも、それ、あなたのせいじゃないんですよ。

こういう「どうしようもないモノリス」を前にして、エンジニアができることって限られてる。

「全部捨てて作り直しましょう!」(ビッグバン・リライト)

なんて提案は、ビジネス的にほぼ100%通りません。だって、作り直してる間、ビジネスは止まるの?既存のバグ修正は誰がやるの?って話だから。

僕らエンジニア、特に海外で結果を出さないといけないサバイバーとしては、もっと「賢く」立ち回る必要があります。

**「人生術」**と言ってもいいかもしれない。

どうやって、この巨大で危険なレガシーシステムと「共存」しながら、ビジネスを止めずに、徐々に、安全に、新しいモダンな世界(クラウドとか、マイクロサービスとか)に移行していくか。

これ、まさに僕が海外の現場で直面して、そして乗り越えようとしている最大の課題です。

ある日、僕らのチームに新しくジョインしたアーキテクトのサラ(ドイツ人)が言いました。

「このモノリス、面白いわね。まるで巨大なイチジクの木みたい」

「…イチジク?」

「そう。**『ストラングラーフィグ(Strangler Fig)』**よ」

ストラングラーフィグ。日本語だと「絞め殺しのイチジク」。

熱帯雨林にある植物で、他の大きな木にツルを巻きつけながら成長し、最終的には宿主の木を覆い尽くし、枯らしてしまう(絞め殺してしまう)植物のことだそうです。

「なにそれ、怖い…」

「でも、これが私たちのアプローチよ。この巨大なモノリス(宿主の木)を、新しいサービス(イチジクのツル)で少しずつ覆っていくの。安全に、確実に、ビジネスを止めずにね」

これが、僕と「ストラングラーフィグ・パターン」との出会いでした。

このブログでは、僕がこの海外の現場で、あのWPFモノリスという「怪物」を相手に、どうやってこの「ストラングラーフィグ」アプローチを実践しているかを、超具体的にシェアしていこうと思います。

これは、単なる技術論じゃありません。

どうやってレガシーコードの「密林」から、安全に切り出せる「独立した機能」を見つけ出すか?

WPFのようなデスクトップアプリの「一部」を、どうやって新しいマイクロサービスのAPIで「包んで」いくか?

そして何より、どうやって「どうせ無理」と諦めているチームの仲間やマネージャーを説得し、この「じわじわ移行」を成功させるか?

そういった、**現場のリアルな「泥臭い戦い方」と「サバイバル術」**の話です。

もしあなたが今、

  • 「触るな危険」のレガシーコードに消耗してる
  • C#やWPFのキャリアにちょっと不安を感じてる
  • 「全部作り直し」以外の現実的な改善策を知りたい
  • 海外でエンジニアとして「価値ある仕事」をしたい

そう思ってるなら、絶対に読んで損はさせません。

これを知ってるだけで、あなたのエンジニアとしての「立ち回り方」が、マジで変わるはずですから。

さて、次回【承】では、まず僕らが最初に取り組んだ、あのカオスなコードベースの密林に分け入り、最初の「ツル」を巻きつけるターゲット(機能)をどうやって特定したか…その「泥臭い分析」の話をします。

密林の解剖学。僕らが「最初の獲物」を見つけるまでの泥臭い戦い

(前回のあらすじ)

ビルドに25分、触れば壊れる15年モノの巨大WPFモノリス。絶望する僕ら海外出稼ぎエンジニアチームの前に現れたドイツ人アーキテクトのサラ。彼女が提案したのは、モノリスを徐々に絞め殺していく「ストラングラーフィグ(絞め殺しのイチジク)」パターンだった。

「言うは易し」とはまさにこのこと。僕らの目の前には、相変わらず「融合」したコードの密林が広がっている。


さて、サラが「ストラングラーフィグよ!」と宣言した翌日のミーティング。

チームの空気は、正直言って「最悪」でした(笑)。

「(また始まったよ、アーキテクト様の理想論が…)」

「どうせ『全部マイクロサービスに置き換えよう!』とか言って、現場が疲弊するパターンだろ」

みんなの顔にそう書いてある。

特に、このシステムの生き字引であるシニアエンジニアのピーター(イギリス人)。彼は腕組みしたまま、一言も発しない。彼が一番、このシステムの「ヤバさ」と「不可能性」を知っているからです。

サラはそんな空気をモノともせず、ホワイトボードにデカデカとこう書きました。

“How to identify and isolate independent functionalities?”

(どうやって、独立した機能を見つけ、分離するか?)

「いい、ヒロ。ピーター。みんな聞いて。ストラングラーフィグの第一歩は、コーディングじゃないわ。『探検』よ

「探検(Exploration)?」

「そう。この巨大なモノリスという『密林』を探検して、どこに『ツル(新しいサービス)』を巻き付け始めるか、最初のターゲットを見つけるの。ここで間違えると、私たちがモノリスを絞め殺す前に、モノリスに絞め殺されるわ」

いや、比喩が怖いよサラ…。

でも、彼女の言いたいことは明確でした。

「全部が全部と繋がってる」ように見えるこの怪物から、どうやって「ここなら切り離せるかも」という、比較的「独立した」機能を見つけ出すか。

これが、僕らの最初の、そして最も重要なミッションになりました。

よくある間違い:「技術レイヤー」で切ろうとすること

僕らエンジニア、特にWPFみたいなMVVM(Model-View-ViewModel)パターンで育ってきた人間は、こういう時つい「技術レイヤー」で切り離そうと考えがちです。

「よし、まずはデータアクセス層(DAL)を全部API化しよう!」

「いや、ビジネスロジック(BL)を別ライブラリに切り出して…」

これ、巨大モノリス相手だと、ほぼ100%失敗します。

なぜなら、そういう「キレイな」層(レイヤー)構造が、もはや存在しないから。

15年の歴史の中で、本来的にはUI(View)にあるべきロジックがViewModelに漏れ出し、ViewModelにあるべきロジックがModelに染み出し、なぜか共通ユーティリティクラス(SharedUtils.cs みたいなやつ)がDBの接続文字列を直接管理してる…なんてのが日常茶飯事。

「起」で僕が死んだのも、まさにそれ。納品書(View)の修正が、在庫管理(Biz Logic?)のグローバル変数(Shared Utils?)を踏み抜いたせいでした。

サラは言います。

「技術レイヤーで切るのは無理。私たちが切るべきは、**『ビジネスの境界(Business Capability)』**よ」

「ビジネスの境界…」

「そう。このシステムは、ビジネス上、何と何をやっているの?『顧客を管理する』『製品カタログを見る』『注文を受ける』『納品書を印刷する』『在庫を管理する』…これらは、本来別々の機能のはずよ」

なるほど。技術的なコードの繋がりじゃなく、ビジネス的な「意味」で分割の単位を探すわけか。

ステップ1:密林の地図作り(という名の説得材料集め)

とはいえ、僕らはビジネスの専門家じゃない。そこで、まずマネージャーのマークを巻き込みました。

「マーク、このシステムって、ビジネス的に一番『困ってる』機能、あるいは『一番変更が多い』機能ってどれ?」

マークは待ってましたとばかりに言いました。

「それなら間違いなく『価格計算エンジン』だ!あそこは毎月のように割引ルールが変わるのに、修正に2ヶ月もかかってる!あそこを分離してくれ!」

…いきなりラスボス来た。

価格計算エンジン。コードを見たら、案の定このモノリスの「心臓部」でした。顧客情報、製品情報、在庫情報、過去の注文履歴…すべてが複雑に絡み合い、もはや「密結合の塊」。

ピーターが冷静にツッコミます。

「マーク、そこを今触ったら、来月の請求書が全部ゼロ円になる可能性があるぞ」

「…それは困る」

サラが仕切り直します。

「OK、マーク。ビジネスインパクト(効果)が大きいのはわかったわ。でも、私たちが今探しているのは、『ローリスク・ハイリターン』な場所。いや、最初は**『ローリスク・ローリターン(低リスク・低効果)』**でもいい」

これ、めちゃくちゃ重要な「人生術」です。

僕らエンジニアは、つい一番カッコよくて、一番難しい問題(ラスボス)から倒しに行きたがる。でも、チームが「どうせ無理」って諦めモードの時にそれをやると、ちょっとした失敗で「ほら、やっぱり無理だったじゃん」とプロジェクト自体が頓挫する。

海外の現場は特にシビア。結果が出ないプロジェクトは、驚くほどあっさり打ち切られます。

だから、最初の成功体験、「あ、これイケるかも」という**「スモールウィン(Small Win)」**を積むことが、チームの士気を上げ、次の(もっと難しい)ステップに進むための「政治力」を確保するために、何よりも重要なんです。

「わかった。じゃあ、ビジネスインパクトはさておき、まずは『安全に』切り離せそうな場所を探そう」

僕らは方針を転換しました。

ステップ2:コードの密林探検(ツールと「勘」を総動員)

さぁ、ここからが僕らC#エンジニアの出番です。

「安全に切り離せる」=「他のモジュールとの依存関係(Dependencies)が少ない」場所を探します。

とはいえ、25分かかるビルドを繰り返しながらコードを全部読むのは不可能。

僕らが使ったのは、以下の「武器」です。

武器1:Visual Studio コードマップ (Code Map)

Visual StudioのEnterprise版にしか入ってないことが多いんですが、これ、マジで便利です。ソリューション全体を解析して、アセンブリ(DLL)間やクラス間の依存関係を、文字通り「地図」として可視化してくれます。

僕らのWPFモノリス(ソリューションファイル .sln)を食わせてみました。

…待つこと15分。

出てきたのは、もはや「地図」ではなく、**「黒いスパゲッティの塊」**でした。

全モジュールが Common.dll や Core.dll といった「神クラス」ライブラリに依存し、その神ライブラリを経由して、結局全員が全員と繋がっている。

「うわぁ…」チーム全員が絶句。

「起」で話した「悪魔のグローバル変数」が、たぶんこの Common.dll の中にいる。

武器2:NDepend (または ReSharper) による静的解析

コードマップが「マクロ(全体図)」なら、こっちは「ミクロ(詳細図)」です。

NDepend(有料ツールですが、ガチな分析には最強)を使って、もっと具体的な「依存元」「依存先」を調べました。

特に注目したのは、「データベース(DB)への直接アクセス」と「外部API呼び出し」、そして**「WPFのUI(View)への直接参照」**です。

もし、あるビジネスロジック(例:ProductCatalogService.cs)が、他のモジュールを一切参照せず、ただDBからデータを読んで返すだけなら…?

それは「独立した機能」の最有力候補です。

解析クエリ(CQLinq)をぶん回します。

「Common.dll を参照 していない クラスを探せ」

→ 結果:0件(絶望)

「じゃあ、Common.dll は参照してるけど、他のビジネスモジュール(Order.dll とか Inventory.dll)を参照 していない クラスは?」

→ 結果:お、いくつか出てきたぞ…?

武器3:生き字引(ピーター)へのヒアリング

ツールが「静的な」依存関係しか教えてくれないのに対し、ピーターは「動的な」依存関係、つまり「実行時にしかわからない繋がり」や「コードには書かれていないビジネスルール」を知っています。

僕:「ピーター、この『製品カタログ表示(ProductCatalogView.xaml)』ってさ、ぶっちゃけ『注文(Order)』モジュールなしでも動くと思う?」

ピーター:「(ふかーいため息の後)…ああ、ProductCatalogView か。あれは厄介だぞ。単体で起動するように見えて、実は『注文画面』から呼び出される時に、SharedConstants.IsOrderMode っていう例のグローバル変数が true になるんだ。その時だけ、カタログの『価格表示』ロジックが変わる。注文モジュールが知らない『割引ルール』を適用するためにな」

出たよ。またお前か、グローバル変数。

ステップ3:ターゲット(最初の獲物)の選定

この泥臭い分析(ツールでの解析と、ピーターへのヒアリング)を2週間続けた結果、僕らはついに、最初のターゲットを見つけ出しました。

それは…「製品カテゴリのツリー表示」機能でした。

地味!めっちゃ地味!(笑)

「価格計算エンジン」に比べたら、ビジネスインパクトなんてほぼゼロ。

WPFアプリの左側にあるナビゲーションペインに表示される、製品の分類(例:『ドリル』→『電動ドリル』→『12Vモデル』)をツリー表示するだけの機能です。

でも、これが「最初の獲物」として最適だったんです。

  • 依存関係が(比較的)少ない:
    • データは専用の「カテゴリマスタ」テーブルから読んでくるだけ。
    • ピーターに確認したところ、「あのツリーは、忌々しいグローバル変数(SharedConstants)の影響を奇跡的に受けていない、この城の唯一の『聖域(Sanctuary)』だ」とのこと。
  • 読み取り専用 (Read Only):
    • データの書き込み(Create/Update/Delete)がない。これが超重要。データの整合性を気にする必要がないため、切り離しのリスクが格段に低い。
  • UIとして独立している:
    • WPFの UserControl(部品化されたUI)として定義されていて、アプリのメインウィンドウ(MainWindow.xaml)にガツッとハマってるだけ。他の画面からポップアップで呼ばれる、みたいな複雑な使われ方をしていなかった。

サラはニヤリと笑いました。

「決まりね。私たちの最初のターゲットは『製品カテゴリ・ツリー』。地味だけど、ここが私たちの『橋頭堡(きょうとうほ:攻略の足がかり)』よ」

「承」のまとめ:まずは「勝てる戦」から始めよ

巨大モノリスという「密林」を攻略する第一歩。

それは、ヒーロー的なコーディングではなく、考古学者のような地味で泥臭い「分析」でした。

ビジネスサイドを巻き込んで「ビジネス境界」を探り、

コード解析ツールで「静的な依存」を暴き、

生き字引のシニアエンジニアから「動的な罠」を聞き出す。

そして何より、チームの士気を上げるために、最初は絶対に失敗しない**「ローリスク・ローリターン」な読み取り専用機能**を狙う。

この「勝ち戦」を選ぶセンスこそが、レガシーシステムと戦うエンジニアにとって、最強の「人生術」なんだと、僕は海外の現場で学びました。

さて、ターゲットは決まった。

いよいよ、僕らの「ツル(新しいサービス)」を伸ばす時です。

この、WPFモノリスの「一部品(UserControl)」としてガッチリ組み込まれている「カテゴリ・ツリー」を、どうやってモノリス本体から引き剥がし、新しいマイクロサービスのAPIで「ラップ」するのか?

これがまた…WPF特有の「見た目(UI)とロジックの癒着」という、第二の壁にぶち当たるんです。

次回【転】、「WPFの壁」とご対面。APIで既存コンポーネントを「包む」ための、僕らの具体的なステップと「アンチパターン」について、ガッツリ語ります。

WPFの壁とAPIの罠。僕らが学んだ「一度に二つ変えるな」という鉄則

(前回のあらすじ)

僕ら海外出稼ぎエンジニアチームは、巨大モノリスWPFアプリの「最初の獲物」として、地味だが安全な「製品カテゴリのツリー表示」機能を選定した。アーキテクトのサラの戦略は、リスクの低い「スモールウィン」を積むこと。いよいよ、この既存コンポーネントを、新しいマイクロサービスAPIで「ラップする(包む)」という、ストラングラーフィグ(絞め殺しのイチジク)の核心的なステップに取り掛かる。


ターゲットは決まった。「製品カテゴリのツリー表示」だ。

ミーティングルームで、僕は意気揚々と宣言した。

「OK、サラ、ピーター。こいつはWPFの UserControl(ユーザーコントロール:UI部品)になってる。MVVMパターン(※)に(一応)なってるから、ProductCatalogTreeViewModel.cs っていうViewModelファイルがある。こいつが、DBからデータを取ってきて、ツリー構造(ObservableCollection<CategoryNodeViewModel>)を作って、画面(View)にバインドしてる。単純だ」

(※MVVM: Model-View-ViewModel。WPFでよく使われる設計パターン。UI(View)とロジック(ViewModel)を分離するのが目的…なのだが、レガシーコードでは大抵これが破綻している)

「だから、やることはシンプル。まず、カテゴリデータを返すだけの新しいAPI(マイクロサービス)を .NET 7 のASP.NET Core でサクッと作って、このViewModelから HttpClient でそのAPIを async/await で叩くように書き換えれば、はい、モノリスから分離完了っしょ!」

僕は、当時イケてると信じていたモダンなやり方を披露した。

これぞ、海外エンジニア。レガシーコードを華麗にモダン化する俺、カッケー。

…しかし。

生き字引のピーターは、またあの「ふかーいため息」をついた。

サラは、僕の目をじっと見て、静かに言った。

「ヒロ。それが一番やっちゃいけない『アンチパターン』よ

「…え?」

アンチパターン(失敗例):張り切りすぎた僕の「API直叩き」地獄

なぜ、僕の提案がダメだったのか?

サラが「ノー」と言ったにもかかわらず、僕は(若気の至りで)「いや、いけるはずだ」と自分のローカル環境でこっそり試してみたんです。

やったこと(失敗例):

  1. 爆速でAPI構築:dotnet new webapi -n CategoryApi でASP.NET Core Web APIのプロジェクトを作成。モノリスが使ってるのと同じDB(のカテゴリマスタテーブル)を読み取り、GET /api/v1/categories でJSONを返すエンドポイントを作った。ここまでは完璧。
  2. ViewModelの魔改造: ターゲットの ProductCatalogTreeViewModel.cs を開く。案の定、コンストラクタ(初期化処理)の中で、あの「悪魔のCommon.dll」経由でDBから同期的に(!)データを取ってくる古いコードが直書きされていた。「うわ、だっさ。こんなの HttpClient で非同期に置き換えてやる」僕はその古いコードをコメントアウトし、代わりに HttpClient を使って、さっき作った GET /api/v1/categories を async/await で呼び出し、返ってきたJSONをデシリアライズしてツリー構造に詰めるコードを書いた。

「よし、動いた!ちゃんとツリーが表示される!モダン化、大成功!」

…そう思ったのは、わずか30分でした。

地獄その1:UIフリーズとパフォーマンスの悪夢

WPFアプリを起動すると、カテゴリ・ツリーが表示されるまで、一瞬(体感0.5秒)だが、**確実にUIが「カクつく」**ようになった。

古いコードは、DBから直接データを読むとはいえ、同じLAN内にあるDBサーバーとのやり取りだったから速かった。

しかし、新しいAPIは「ネットワーク越しのHTTP通信」。どんなに速いAPIでも、ネットワークの遅延(レイテンシ)や、JSONのシリアライズ・デシリアライズのコストが乗っかる。

WPFのようなデスクトップアプリは、UIスレッド(画面を描画するメインスレッド)が命。async/await を使って非同期にしたつもりでも、起動時のコンストラクタで呼んでしまったり、Result プロパティで無理やり同期的に待ってしまったりする(レガシーコードあるある)と、その瞬間にUIスレッドがブロックされ、ユーザー体験は最悪になる。

ピーター:「(僕のPCの画面を覗き込み)…ヒロ、クリックするたびにマウスカーソルが『砂時計』になってるぞ。これじゃユーザーからクレームの嵐だ」

地獄その2:認証・認可の迷宮

僕らのモノリスWPFアプリは、Windows認証(Active Directory)で動いていた。「誰がアプリを使っているか」は、Windowsのログイン情報で自動的に管理されていた。

しかし、僕が新しく作ったAPIは、デフォルトのまま。つまり「認証なし」か、せいぜいJWTトークン認証を想定していた。

ローカル環境では動いた。でも、ステージング環境(本番に近い環境)にデプロイした途-たん、APIが「401 Unauthorized(認証エラー)」を返してきた。

モノリス(WPSアプリ)が持ってるWindows認証の「資格情報」を、どうやって新しいAPI(マイクロサービス)に引き継ぐんだ?HttpClient に UseDefaultCredentials = true をつければいい?いや、APIサーバー側がWindows認証を受け付ける設定になってない…。

セキュリティという、まったく別の巨大な問題に直面してしまった。

地獄その3:デバッグ(切り分け)の崩壊

一番最悪だったのがこれ。

「ヒロ、カテゴリ・ツリーの一部が表示されないってバグ報告が来てるぞ」

「え!?…ローカルでは動いたのに…」

さぁ、原因調査だ。

これは、モノリス(WPF)のViewModelのバグか?JSONのデシリアライズ処理のミスか?それともネットワークの問題か?ファイアウォールでブロックされてる?それとも、新しいAPI側のロジックがバグってる?

僕は、モノリスのデバッガと、APIサーバーのログと、ネットワーク監視ツール(Fiddler)を同時に睨めっこするハメになった。

「一度に二つのことを変えた」

これが、僕の致命的な失敗でした。

僕は、「①DBアクセスロジックの置き換え」と「②ネットワーク越しのAPI呼び出しへの変更」という、まったく性質の異なる二つの変更を、同時にやってしまった。

だから、問題が起きた時に、どっちが原因か特定できなくなったんです。

「転」の核心:『ファサード』で、まず「壁」を作れ

ミーティングルームで、僕のローカルでの惨状を報告すると、サラは「だから言ったでしょ」という顔で、ホワイトボードに向かった。

「ヒロ。いい教訓になったわね。ストラングラーフィグ(絞め殺し)は、一気に首を絞めたら宿主(モノリス)が暴れて、こっちがやられるのよ」

「じゃあ、どうすれば…」

「**『ファサード(Facade)』**よ。まず、モノリスの中に『壁』を作るの」

サラが示した、正しい「ラップ」の実践的ステップはこうだ。

ステップ1:API(ツル)を作る(※これはさっきと同じ)

GET /api/v1/categories を返すASP.NET Core Web APIを作る。これはOK。ただし、まだWPFアプリからは「呼ばない」。こいつはまだ出番待ち。

ステップ2:モノリス側に「インターフェース」を定義する

これが最重要。

モノリスのWPFプロジェクト(あるいは共通ライブラリ)に、新しいインターフェースを1枚切る。

C#

// ICategoryService.cs
// これから「カテゴリ情報が欲しい」時は、必ずこのインターフェース経由で要求する、という「新しいルール」を作る。
public interface ICategoryService
{
// ViewModelが欲しいのは、結局これ(ツリー構造のデータ)だけ。
Task<IEnumerable<CategoryNode>> GetCategoryTreeAsync();
}

ステップ3:インターフェースの「古い実装(ファサード)」を作る

次に、このインターフェースの実装クラスを作る。

こいつが「ファサード(建物の正面)」=「ラップ(包むもの)」になる。

C#

// LegacyCategoryServiceFacade.cs
// 「ファサード」クラス。中身は「古いロジック」のまま。
public class LegacyCategoryServiceFacade : ICategoryService
{
public async Task<IEnumerable<CategoryNode>> GetCategoryTreeAsync()
{
// ★★★ココが最重要★★★
// 既存の ProductCatalogTreeViewModel.cs に直書きされていた
// 「あの忌々しい Common.dll 経由でDB直叩き」の
// 古いコードを、そのままコピペしてくる!
// (あるいは、古いクラスを内部で呼び出す)

// (例:古いロジックがこんな感じだったとする)
var oldData = LegacyDbAccessor.GetCategoriesFromDb();
var tree = BuildTreeFromOldData(oldData);

// 非同期インターフェースに合わせるため、形だけ Task.FromResult で包む
return await Task.FromResult(tree);
}
}

ステップ4:ViewModel を「インターフェース依存」に書き換える

最後に、ターゲットの ProductCatalogTreeViewModel.cs を書き換える。

でも、HttpClient でAPIを呼ぶコードにはしない。

C#

// ProductCatalogTreeViewModel.cs (改修後)
public class ProductCatalogTreeViewModel : BaseViewModel
{
private readonly ICategoryService _categoryService;
public ObservableCollection<CategoryNodeViewModel> CategoryTree { get; }

// ★コンストラクタで「インターフェース」を受け取るようにする(DI:依存性の注入)
public ProductCatalogTreeViewModel(ICategoryService categoryService)
{
_categoryService = categoryService;
CategoryTree = new ObservableCollection<CategoryNodeViewModel>();
LoadCategoriesAsync(); // 非同期でロード
}

private async void LoadCategoriesAsync()
{
// ★古いロジック(DB直叩き)は完全に消えた!
// ★ViewModel は「インターフェース」を呼ぶだけ。
// この裏側が「古いロジック」なのか「新しいAPI」なのか、ViewModel は知らない。
var nodes = await _categoryService.GetCategoryTreeAsync();

// (あとは受け取ったデータを画面用のViewModelに詰め替える処理...)
}
}

(※DIコンテナ(Unityとか)を使って、ICategoryService が呼ばれたら LegacyCategoryServiceFacade のインスタンスを渡すように設定する)

これが、本当の「ラップ」だ。

何が起きたかわかりますか?

僕らは、WPFアプリ(ViewModel)の動作(振る舞い)を一切変えていない。

LegacyCategoryServiceFacade の中身は、古いロジックそのものだから。

でも、僕らは「壁」を作った。

ProductCatalogTreeViewModel は、もう「DB直叩き」という「レガシーな実装」を知らなくなった。彼は ICategoryService という「抽象的な窓口」とだけ会話するようになったんです。

これが「WPFの壁(UIとロジックの癒着)」を安全に引き剥がす、唯一の方法でした。

「サラ…すごい。これなら、パフォーマンスも変わらないし、認証の問題も起きない。だって、何も変えてないんだから」

「そうよ、ヒロ。私たちは今、**『切り替えスイッチ』**を手に入れたの」

彼女の言う通り。

僕らは今、ICategoryService という「切り替えスイッチ」を手に入れた。

今は、このスイッチは「古いロジック(Legacy)」側に倒れている。

でも、もし僕らが、新しいAPI(GET /api/v1/categories)を呼ぶ、別の実装クラスを作ったら?

C#

// NewApiCategoryService.cs (未来の実装)
public class NewApiCategoryService : ICategoryService
{
private readonly HttpClient _httpClient;

public NewApiCategoryService(HttpClient httpClient)
{
_httpClient = httpClient;
}

public async Task<IEnumerable<CategoryNode>> GetCategoryTreeAsync()
{
// こっちは「新しいAPI」を呼ぶ!
// (認証やパフォーマンスの問題は、このクラスの中で頑張って解決する)
var json = await _httpClient.GetStringAsync("/api/v1/categories");
var nodes = JsonConvert.DeserializeObject<IEnumerable<CategoryNode>>(json);
return nodes;
}
}

…そう。

あとは、DIコンテナの設定を LegacyCategoryServiceFacade から NewApiCategoryService に切り替えるだけで、ViewModel に一行も触れることなく、裏側のロジックを「古いDB直叩き」から「新しいAPI呼び出し」に差し替えることができる。

これが、ストラングラーフィグ・パターンにおける「既存コンポーネントのラップ」の実践的なステップでした。

「一度に二つ変えるな」。

「まず『抽象(インターフェース)』という壁で『ラップ』しろ」。

「『差し替え』は、その後でやれ」。

この海外の現場で学んだ「人生術」は、僕のエンジニアリングのアプローチを根底から変えることになりました。

さて、スイッチは手に入れた。

でも、いつ切り替える?いきなり全ユーザーを「New」に切り替えて、また地獄を見たら?

そう。僕らの戦いはまだ終わらない。

次回【結】。この「スイッチ」をどう安全に切り替えていくか。モノリスとマイクロサービスを「並行稼働」させ、僕らがどうやって「本当の勝利」を掴んだのか。その現実的な移行戦略(マイグレーション)の例について、お話しします。

「並行稼働」という名の安全綱。僕らが手にした「レガシーから卒業する権利」

(前回のあらすじ)

僕(ヒロ)は、WPFモノリスの「製品カテゴリ・ツリー」機能を分離するため、HttpClient でAPIを直叩きして大失敗。「一度に二つ変えるな」というサラの教えのもと、「インターフェース(ICategoryService)」という「壁」を作り、古いロジック(Legacy…Facade)と新しいAPIロジック(NewApi…Service)を差し替え可能にする「切り替えスイッチ」を手に入れた。しかし、このスイッチ、いつ、どうやって切り替えるのが正解なんだ?


「切り替えスイッチ」は手に入れた。

ICategoryService というインターフェースをDI(依存性の注入)コンテナに登録する時、LegacyCategoryServiceFacade を指すか、NewApiCategoryService を指すか。

「よし、サラ!切り替えの準備はできた。明日の朝、リリースする。DIコンテナの設定を『New』に切り替えるぞ!」

僕がそう言うと、サラはまた、あの「やれやれ」という顔で首を振った。

「ヒロ。それじゃ、あなたの『API直叩き』の失敗と、リスクの大きさが何も変わってないわ」

「え…?でも、インターフェースで分離したんだから、問題あればすぐ『Legacy』に戻せるじゃないか」

「『問題あれば』?その問題は誰が検知するの?全ユーザーが一斉にバグを踏んで、サポートデスクが炎上してから?私たちは『ストラングラー(絞め殺し)』であって、『爆弾魔(Bomber)』じゃないのよ」

「結」の核心:フィーチャートグル(Feature Toggle)という最強の「人生術」

サラが提案したのは、この移行戦略における、まさに「切り札」でした。

それは、**「フィーチャートグル(Feature Toggle)」**という手法です。

聞いたことありますか?

ざっくり言うと、「コードの中に埋め込む、機能のON/OFFスイッチ」のこと。

僕らがやったのは、ICategoryService の実装を「どっちか」に決めることではありませんでした。

ICategoryService の「第三の、そして最強の実装」を作ったんです。

C#

// SmartCategoryService.cs (第三の実装)
// こいつが「賢い」切り替えスイッチ本体になる
public class SmartCategoryService : ICategoryService
{
private readonly ICategoryService _legacyService; // 古いロジック
private readonly ICategoryService _newApiService; // 新しいAPIロジック
private readonly IFeatureToggleService _toggleService; // ON/OFFを管理するサービス

// 古いのも新しいのも、両方DIで受け取る
public SmartCategoryService(
LegacyCategoryServiceFacade legacyService,
NewApiCategoryService newApiService,
IFeatureToggleService toggleService)
{
_legacyService = legacyService;
_newApiService = newApiService;
_toggleService = toggleService;
}

// ★★★ココが核心★★★
public async Task<IEnumerable<CategoryNode>> GetCategoryTreeAsync()
{
// まず、トグルサービスに「今、新APIを使っていいか?」と尋ねる
if (_toggleService.IsNewCategoryApiEnabled())
{
// --- ONの場合 ---
try
{
// 新しいAPIを叩きにいく
return await _newApiService.GetCategoryTreeAsync();
}
catch (Exception ex)
{
// もし、新しいAPIがコケたら...(例:ネットワーク障害、APIが500エラー)
// ログだけ記録して...
Log.Error("New Category API failed!", ex);

// ★フォールバック(安全綱)★
// こっそり古いロジックを動かして、ユーザーにはエラーを見せない!
return await _legacyService.GetCategoryTreeAsync();
}
}
else
{
// --- OFFの場合 ---
// 何もせず、今まで通り古いロジックを動かす
return await _legacyService.GetCategoryTreeAsync();
}
}
}

そして、DIコンテナには、この SmartCategoryService を ICategoryService の実装として登録する。

何が起きたか。

僕らは、**「並行稼働(Parallel Run)」と「安全綱(Fallback)」**を手に入れたんです。

ViewModel(ProductCatalogTreeViewModel)は、相変わらず ICategoryService を呼んでるだけ。

その裏側で、この「賢いスイッチ」が、新しいAPIがONかOFFかを判断し、もしONで、かつAPIが失敗したとしても、自動的に古いロジックが動いてくれる。

これ、最強じゃないですか?

WPFアプリ(モノリス)の安定性を100%担保したまま、新しいAPI(マイクロサービス)を「試す」ことができるようになったんです。

実録:僕らの「リアルな移行」ステップ

この「賢いスイッチ」を握りしめ、僕らはいよいよ、現実世界での移行(マイグレーション)を開始しました。これが、僕が体験した「リアルなストラングラーフィグ」です。

ステップ1:カナリア・リリース(開発チームが毒見役)

IFeatureToggleService の中身をこう実装しました。

「もし、今ログインしているユーザーが『ヒロ』か『サラ』か『ピーター』(つまり開発チーム)だったら、IsNewCategoryApiEnabled() は true を返せ。それ以外は false を返せ」

そして、本番環境(Production)にリリース!

…案の定、僕らの画面だけ、カテゴリ・ツリーが正しく表示されませんでした(笑)。

「転」で僕がハマった「認証」の問題が、本番環境で再発したんです。

でも、被害は僕らだけ。他の全ユーザーは、今まで通り古いロジック(Legacy…)が動いているので、何も気づかない。

僕らは慌てず、NewApi…Service 側の認証ロジックを修正し、再デプロイ。

ついに、僕らの画面で「新しいAPIから取得したカテゴリ・ツリー」が表示されました。

ピーターが「…おお。動いてる。しかも、古いコードより速いぞ」と呟いた時、マジでガッツポーズでしたね。

ステップ2:パワーユーザーへの展開

次に、トグルサービスを書き換え。

「開発チーム」に加えて、「経理部のアンナさん(PCに詳しいパワーユーザー)」も true に。

「アンナ、なんかツリー表示、変なとこない?」

「ええ、特に。あ、でも前より一瞬ロードが速くなったかも?」

よっしゃ!

ステップ3:段階的ロールアウト

次に、トグルサービスをさらに書き換え。

「全ユーザーのうち、ユーザーIDの末尾が『1』の人だけ true にする」(=全ユーザーの10%)

これで1週間、APIの負荷やエラーログを監視。問題なし。

「じゃあ、末尾が『1』~『5』の人」(=50%)

そして、リリースから1ヶ月後。

ついに、IsNewCategoryApiEnabled() の中身を、**「常に true を返す」**に書き換えたんです。

全ユーザーが、新しいAPIを使うようになった。

古いロジック(LegacyCategoryServiceFacade)は、万が一のための「安全綱」として、まだコードの中に残っています。

僕らが手にした「本当の勝利」

僕らは、モノリスを倒したわけじゃない。

でも、モノリスの「一部(カテゴリ・ツリー)」を、安全に、誰にも迷惑をかけず、ビジネスを止めることなく、「絞め殺す」ことに成功したんです。

モノリスの中にあった古いコードは、もう二度と実行されることはない(APIが落ちない限り)。

それは事実上、「死んだ」コードになりました。

これが、ストラングラーフィグ・パターンの「結」です。

このアプローチを知って、僕のエンジニア人生は変わりました。

もう、巨大なレガシーコードを前に「どうせ無理だ」「全部作り直すまで待つしかない」と諦める必要はなくなった。

エンジニアは、「レガシーから卒業する権利」を、自分たちの手で掴み取れるんです。

このブログを読んでくれた、かつての僕と同じようにレガシーシステムに消耗しているあなたへ。

「ストラングラーフィグ」は、単なる技術パターンじゃありません。

それは、**「どうやって賢く、リスクを最小限にして、現実を変えていくか」**という、最強の「人生術」です。

  • **「ビジネス境界」**で切り出せる、小さな獲物(ローリスク・ローリターン)を見つけること。
  • **「インターフェース」**という壁で、古い実装と新しい実装を「ラップ」すること。
  • **「フィーチャートグル」**という安全綱を使い、古いものと新しいものを「並行稼働」させること。

これさえ知っていれば、あなたはもう「コードの奴隷」じゃない。

巨大なモノリスを少しずつ、確実に「飼いならす」ことができる、**「レガシー・テイマー(Legacy Tamer)」**です。

僕らの戦いはまだ始まったばかり。モノリスはまだデカい。

でも、僕らは最初の「ツル」を巻き付けた。

次は、どの機能を「絞め殺し」に行こうか。ピーターとサラと、次の「獲物」を探す僕らの目は、もう絶望していません。

コメント

タイトルとURLをコピーしました