海外でWPFアプリ開発してる俺が気づいた、技術力より大事な「カルチュラル・コード・シンクロナイゼーション」って話。

  1. そのコード、世界共通語だと思ってない?
      1. 忘れもしない、あの「炎上コードレビュー」
      2. 「技術力」はあった。じゃあ何が「ズレ」ていたのか?
      3. C#は共通語。でも「書き方」は方言だらけ。
  2. コードレビューは戦場じゃない。多国籍チームで「わかってる」と思われるための対話術。
      1. ステップ1:プライドを捨て、質問者になれ(炎上鎮火フェーズ)
      2. 俺が編み出した「多国籍コードレビュー」の人生術 3ヶ条
        1. ルール1:PRは「完成品(ドヤ顔)」を出すな。「叩き台(不安顔)」で出せ。
        2. ルール2:「指摘(Fact)」と「人格攻撃(Fiction)」を切り離せ。
        3. ルール3:「Emoji(絵文字)」と「GIF」を舐めるな。
  3. なぜ誰も読まない?アーキテクチャを「グローバル言語」に変えるドキュメント術。
      1. 完璧な「小説」を書いて、コオロギが鳴いた日
      2. C4モデルが俺を救った。「グローバル言語」の正体
      3. 「わかってるヤツ」になるためのドキュメント術 3ヶ条
        1. 1. テキストは負債。図は資産。C4モデルを崇拝せよ。
        2. 2. ドキュメントは「Wiki」に置くな。「Git」に置け。
        3. 3. ドキュメントは「コード」と一緒に「レビュー」させろ。
  4. 「俺の環境じゃ動く」を撲滅せよ。地球の裏側でも動く「ローカル環境ツイン」戦略。
      1. なぜ「ズレ」はまた起きたのか
      2. C#/WPF開発でも絶対やるべき「ローカル環境ツイン」戦略 3ヶ条
        1. 1. 「設定ファイル」を「環境変数」で上書き可能にしろ
        2. 2. データベースとAPIは「Docker」で起動しろ
        3. 3. 「run.ps1」ですべてを起動可能にしろ
      3. 結論:「シンクロ」こそが最強の「人生術」だ

そのコード、世界共通語だと思ってない?

(ここから本文:約3000文字)

ども!ヨーロッパの片隅で、今日もXAMLと格闘してるC#/WPFエンジニアです。

突然だけど、みんな、海外で働くITエンジニアって聞くと、どんなイメージ持つ?

やっぱり、最先端のWeb企業で、多国籍チームに混じって、英語で華麗にディスカッションしながらイケてるサービスを…みたいな感じ?

まあ、半分合ってるし半分違う。

俺が今メインでやってるのは、ゴリゴリのBtoB。特定の産業(製造業とか医療系とかね)で使われる専門的なデスクトップアプリケーション。そう、WPF。Windows Presentation Foundation。

「え、今どきWPF?」って思った?

いやいや、こっち(海外)でも、ことエンタープライズのWindowsアプリ開発においては、WPFはまだまだ現役バリバリの主役だぜ。XAMLでUIを組んで、MVVMパターンでロジックを分離して、DI(Dependency Injection)でガチガチに疎結合にする。日本でやってたことと、技術スタック自体は驚くほど変わらない。

でね、俺もこっちに来る前はこう思ってたんだ。

**「プログラミング言語は世界共通。C#さえ書ければ、コードが俺の名刺代わり。どこでもやっていけるっしょ」**って。

……マジで、甘かった。

こっち来て数年経つけど、あの頃の自分に言ってやりたい。

「お前がぶつかる壁は、NullReferenceException より厄介な『見えない壁』だぞ」と。

技術力はもちろん大前提。それは疑いようがない。

でも、こっちの多国籍チームで「コイツ、できるな」って信頼を勝ち取るためには、技術力と同じか、もしかしたらそれ以上に大事なスキルセットがあったんだ。

俺はそれを、勝手に**「カルチュラル・コード・シンクロナイゼーション(Cultural Code Synchronization)」**、略してCCSって呼んでる。

文化的な背景と、俺たちが書くコード(あるいはその周辺作業)を、意図的に「同期」させる技術。

今日は、俺がこのCCSの重要性に気づかされるキッカケになった、クソ恥ずかしい失敗談から話そうと思う。

これは、これから海外を目指すC#エンジニアにも、今まさに多国籍チームで苦労してる人にも、絶対役立つ「人生術」だ。


忘れもしない、あの「炎上コードレビュー」

こっちの会社に入って半年くらい経った頃。

俺もだいぶ環境に慣れてきて、「よし、そろそろ本気出すか」なんて思ってた時期だ。

ある日、パフォーマンス改善のタスクが回ってきた。

WPFアプリのある画面。DataGrid に数万件のデータを表示するんだけど、起動やらスクロールやらがとにかくモッサリしてる。ユーザーからクレームが来てるから、なんとかしろ、と。

「おう、任せとけ」と。

日本でも散々やってきた、WPFのパフォーマンスチューニング。腕が鳴るぜ。

俺は持てる知識を総動員した。

  • まずは基本、DataGrid のUI仮想化(Virtualization)。EnableRowVirtualization="True" はもちろん、VirtualizationMode="Recycling" に設定。
  • ボトルネックになってたのは、データの読み込みと、Gridに表示するためのデータ変換処理(IValueConverter)だと特定。
  • よっしゃ、データ読み込みは非同期だ。画面表示をブロックしちゃダメだろ。ViewModelの Loaded イベント(まあ、実際は ICommand にバインドされたメソッドだけど)で、async void を使って、await Task.Run(…) で重いデータフェッチ処理をバックグラウンドスレッドに逃がす。UIスレッドはこれで固まらない。完璧。
  • IValueConverter も、Binding のたびに無駄な変換が走りまくってたから、変換結果をキャッシュするロジックをサクッと追加。

自分なりに「どうだ、これが日本のカイゼンだ!」ってくらいの会心の出来。

ローカルで動かしても爆速。よっしゃ、これで文句ないだろ!と、自信満々でPull Request(PR)を投げた。

数時間後、PRに通知が溜まってる。

「お、さすがに賞賛の嵐か?」なんてウキウキしながら開いたら、そこは戦場だった。

リードエンジニアのピーター(仮名・ドイツ人。アーキテクチャに鬼のように厳しい)からの一言。

“This is fundamentally wrong.”

(これ、根本的に間違ってる)

……え?

根本的? パフォーマンスは上がってるんだぞ?

そこから、チームメンバー(インド人、ポーランド人、ブラジル人…まさに多国籍軍)からも、コメントの嵐。

ラヴィ(インド人シニアエンジニア):

「なんで Task.Run をViewModelで直接呼んでるんだ? ViewModelの責務はUIの状態保持とロジックの呼び出しだけだ。

データアクセスや重いビジネスロジックは、DIで注入された IService 層でやるべきだろ。MVVMの責務分離に違反してる」

ピーター(再び):

「そもそも、async void は絶対に使うな。

例外がキャッチできず、アプリケーション全体をクラッシュさせる温床だ。ICommand で非同期処理を実行するなら、我々の共通ライブラリにある AsyncDelegateCommand(Task を返す非同期コマンドの実装)がなぜ使えない? まさか、知らなかったとは言わせないぞ?」

アンナ(ポーランド人QAエンジニア):

「(コードレビューじゃないけど)ていうか、このブランチをQA環境にデプロイしたけど、画面真っ白でデータ何も表示されないんだけど。ログに System.Data.SqlClient.SqlException が大量に出てる。

あなたのローカルDBの接続文字列、App.config にハードコードしてない?」

………。

もうね、グウの音も出ない。

俺のプライドも、書いたコードもズタズタ。

「技術力」はあった。じゃあ何が「ズレ」ていたのか?

ショックだった。

俺は「パフォーマンス改善」というタスクを、技術的に(C#の機能を使って)解決したつもりだった。

async/await も知ってる。DIの概念も知ってる。App.config の環境ごとの使い分けも、もちろん知ってる。

でも、彼らが指摘したのは、そういう個別の技術要素じゃない。

もっとデカい、**「チームとしての働き方の規約(ルール)」と「設計思想(アーキテクチャ)」**だったんだ。

彼らにとっての「良いコード」と、俺にとっての「良いコード」の**前提(コンテキスト)**が、まるで違ってた。

  1. 責務分離(MVVM)への異常なこだわり:彼らのチーム(というか、欧米のエンタープライズ開発)では、MVVMパターンの責務分離は「絶対」。ViewModelはUIのためだけ。ロジックはService層。データアクセスはRepository層。これが鉄の掟。なぜなら、チームメンバーが地球の裏側にいても(分散チーム)、QAチームが別拠点でも、**「誰が書いても同じ構造になる」**ことを最重要視してるから。俺みたいに「ちょっとパフォーマンス上げたいから」ってViewModelで Task.Run なんてやらかすのは、「お前のせいでルールが壊れるだろ!」って話なんだ。
  2. async void への憎悪:async void は、WPFのイベントハンドラ(例: Button_Click)みたいに、UIフレームワーク側が void しか許容しない場合に「仕方なく」使うもの。それ以外で(特にViewModelのコマンドで)使うのは、「俺は例外処理を放棄します」と宣言してるのと同じ。多国籍チームでは、誰が書いたかわからんコードが原因でアプリが落ちるのが一番最悪。だから、堅牢性(ロバストネス)に関する規約は、日本のチームの比じゃないくらい厳しい。
  3. 環境の分離(「俺の環境」は存在しない):「俺の環境じゃ動く」は、万国共通のクソ台詞オブザイヤーだ。特に、CI/CD(継続的インテグレーション/デリバリー)パイプラインがガチガチに整備されてる環境では、ローカル開発環境、QA環境、本番環境の config が分離されてるのは大前提。アンナがQA環境でテストできない時点で、俺の仕事は「未完了」どころか「マイナス」評価だ。

C#は共通語。でも「書き方」は方言だらけ。

この経験で学んだのは、これだ。

「コード(C#)は世界共通語だけど、コードの書き方(作法)は、恐ろしくローカル(文化的)な方言だらけ」

英語が話せても、ネイティブのイディオムやスラング、ビジネス上の言い回しを知らないと会話がチグハグになるのと、まったく同じ。

C#が書けても、そのチームの「イディオム(=設計思想、アーキテクチャ規約、暗黙のルール)」を理解してないと、まともにコードレビューも通らない。

これ、マジでヤバくない?

日本でどれだけ優秀なC#/WPFエンジニアだったとしても、海外来た途端に、この「文化のズレ」のせいで「使えないヤツ」扱いされる可能性が、ここにあるんだ。

技術力は高いのに、評価されない。チームに貢献できない。最悪だろ?

でも、これって別にピーターが意地悪だったわけでも、俺の技術が低かったわけでもない。

(いや、App.config ハードコードは俺が100%悪いけど)

ただ、**「ズレ」**てただけなんだ。

俺たちは、この「ズレ」を、もっと意図的に、もっとシステマティックに「同期(シンクロ)」させる必要がある。

それが、俺が提唱する**「カルチュラル・コード・シンクロナイゼーション(CCS)」**だ。

この「ズレ」、どうやって解消する?

「郷に入っては郷に従え」で、ひたすら既存コードを読みまくって、そのチームのマイルールを学ぶ?

それも大事。でも、それじゃ時間がかかりすぎる。チームがデカかったり、複数の国にまたがってたら(俺のチームみたいに)、その「郷」が多すぎてカオスだ。

もっと効率よく、この「ズレ」を最小限にする方法が必要なんだ。

このブログでは、俺があの炎上PRから数年間、多国籍チームでC#/WPFエンジニアとして働きながら編み出した、超具体的な「CCS(カルチュラル・コード・シンクロナイゼーション)術」を、3つの側面に分けて徹底的に解説していく。

海外で働きたいヤツも、今まさに多国籍チームで苦労してるヤツも、絶対読んで損はさせない。

これを知ってるだけで、無駄なコードレビューの炎上を防げるし、何より「コイツ、わかってるな」ってチームからの信頼を爆速でゲットできる。

俺がボコボコにされながら学んだ「人生術」だ。

次回からの「承」「転」「結」で、出し惜しみなく全部話す。

具体的には、この3つのテーマだ。

  1. Bridging communication gaps(コミュニケーションギャップの解消):まずはレビューだ。あの炎上したPRを、俺がどうやって乗り越え、ピーターやラヴィの信頼を勝ち取ったか。レビューを「戦場」から「共創の場」に変えるための、超具体的なコードレビューの作法と対話術。(次回:承:コードレビューは戦場じゃない。多国籍チームで「わかってる」と思われるための対話術。)
  2. Documentation as a global language(グローバル言語としてのドキュメンテーション):「ルールがあるなら書いとけよ!」ってキレる前に。そもそも「読まれる」ドキュメントって何だ? アーキテクチャ図(俺はC4モデル派)から設計思想まで、非同期でも伝わる「グローバル言語」としてのドキュメント術。(次々回:転:なぜ誰も読まない?アーキテクチャを「グローバル言語」に変えるドキュメント術。)
  3. The “local environment twin”(ローカル環境ツイン):アンナを二度と怒らせないために。「俺の環境じゃ動く」を撲滅する。地球の裏側にいるQAチームでも一発で動く環境をどう作るか。WPFアプリ特有のDB接続や外部APIモック化も含めた、「ローカル環境ツイン」戦略。(最終回:結:「俺の環境じゃ動く」を撲滅せよ。地球の裏側でも動く「ローカル環境ツイン」戦略。)

この記事を読み終える頃には、君も「文化のズレ」を恐れるんじゃなく、それを乗りこなす「シンクロ術」を身につけてるはずだ。

じゃ、今日はここまで。

コードレビューは戦場じゃない。多国籍チームで「わかってる」と思われるための対話術。

(ここから本文:約3000文字)

さて、前回の「起」で、俺の自信作パフォーマンス改善PRが、リードエンジニアのピーター(ドイツ人)による “This is fundamentally wrong.” という一言で火の海になった話を覚えてるか?

ラヴィ(インド人)からは「MVVMの責務違反」、アンナ(ポーランド人)からは「ローカルDBの接続文字列ハードコード」という、四方八方からのフルボッコ。

正直、あの通知の嵐を見た瞬間、頭に血が上った。

「いやいや、パフォーマンスは上がっただろ!」

「async void がダメ? でも動いてるじゃん!」

「こっちの共通ライブラリとか、まだ全部把握してるわけねーだろ!」

……って、一瞬なった。マジで。

日本でそれなりにやってきたっていう自負が、音を立てて崩れていく。

あの時、俺が取れる道は3つあったと思う。

  1. (最悪)ブチギレて反論: 「俺の実装は技術的に正しい! お前らのローカルルールが間違ってる!」とコードで殴り合う。
  2. (ダメ)黙って全部修正: 「スミマセン…」とだけ書いて、言われた通りに全部直す。なぜ怒られたのか、本質を理解しないまま。
  3. (俺が選んだ道)火を消し、穴を掘る: 冷静になって、まず「火事」を消し、その「火種」がどこから来たのかを徹底的に掘り下げる。

もちろん、俺は「3」を選んだ。

海外の多国籍チームで生き残るってのは、技術力でマウントを取ることじゃない。

いかに「ズレ」を素早く察知し、賢く「同期(シンクロ)」できるかにかかってる。

今日は、あの地獄のPRを、どうやって「神PR」とまではいかなくても(笑)、「チームの信頼を勝ち取るPR」に変えていったか。

その超具体的なステップと、俺が編み出した「多国籍コードレビューの人生術」を話そう。

これはC#の知識というより、完全に「サバイバル術」だ。


ステップ1:プライドを捨て、質問者になれ(炎上鎮火フェーズ)

まず、俺がPRに返したコメント。

一番やっちゃいけないのは、ピーターの “fundamentally wrong” に対して、「何がWrongなんだ?具体的に言え!」と、ケンカ腰で返すこと。

テキストコミュニケーション、しかも文化が違うと、こっちの「イラつき」は10倍になって伝わる。

俺は、深呼吸して、こうコメントした。

“Hi team, thanks for the massive feedback! 🙏

Honestly, I’m a bit overwhelmed. It seems I missed some important architectural rules here.

@Peter, you mentioned “fundamentally wrong.” Could you elaborate on that? I thought moving the data fetch off the UI thread with Task.Run was a good performance win, but I’m clearly missing the team’s approach. What’s the standard way to handle async operations in our ViewModels?

@Ravi, I see your point about MVVM separation. Is the rule here that ViewModels should never know about Task or async, and we should inject an IService that returns the data (or maybe an IObservable)?

My apologies for the App.config issue, @Anna. That was a stupid mistake. Fixing it right now. 🤦‍♂️”

日本語にすると、こんな感じだ。

「みんな、大量のフィードバックありがとう!🙏

正直、ちょっと圧倒されてる。俺、どうやら大事なアーキテクチャのルールを見逃してたみたいだ。

ピーター、”根本的に間違ってる”って部分、もうちょい詳しく教えてくれない? Task.Run でUIスレッドから処理を逃がすのは良い改善だと思ったんだけど、明らかにチームのやり方と違うみたいだ。ViewModelでの非同期処理って、このチームではどうやるのが『標準』なの?

ラヴィ、MVVMの責務分離の指摘、理解した。このチームのルールは『ViewModelはTaskやasyncを一切知るべきではない。Service層をDIで注入して、そこがデータを返す(あるいはIObservableとか?)べき』って感じ?

アンナ、App.config の件はマジでごめん。アホなミスだった。今すぐ直す。🤦‍♂️」

ポイントがいくつかある。

  1. まず感謝と謝罪: レビューしてくれた「時間」に感謝する。ミス(特にApp.configみたいな分かりやすいミス)は、変な言い訳せず即謝る。
  2. 感情(圧倒されてる)の開示: 「俺、今ちょっとパニクってる」って素直に言う。これで相手は「ああ、こいつを攻撃したいわけじゃないんだ」って冷静になる。
  3. 「何を」じゃなく「なぜ」を聞く: 「async void がダメ」じゃなく、「なぜ async void がダメなのか」「じゃあ『標準』はどうなのか」という、**背景(コンテキスト)**を聞く姿勢を見せる。
  4. 相手の指摘を「自分の言葉で」復唱する: ラヴィへの返信のように、「あなたの指摘は、こういう理解で合ってる?」と確認する。これは「あなたの話をちゃんと聞いてますよ」という最強のシグナルだ。

このコメントで、戦場だったPRの雰囲気が一瞬で変わった。

ピーター(ドイツ人)は、こういう「ロジック」と「ルール」を求める質問が大好きだったんだ。

彼から、驚くほど丁寧な返信が来た。

「(意訳)よくぞ聞いた。async void を我々が憎むのは、それが例外を捕捉せず、AppDomain 全体をクラッシュさせるからだ。過去にそれで週末を失ったメンバーがいる。

我々のチームでは、ICommand の非同期実装として、例外処理と IsBusy プロパティの制御をカプセル化した AsyncDelegateCommand という共通ライブラリを必ず使うルールになってる。これを使えば、ViewModelは Task.Run なんて知る必要はない。知るべきでもない」

ラヴィ(インド人)からも。

「(意訳)その理解で合ってる。ViewModelは『馬鹿』であるべき(Dumb ViewModel)だ。UIの状態(IsLoadingフラグやDataGridにバインドするObservableCollection)を持つだけ。データ取得ロジックはすべてIFooServiceに隠蔽する。なぜなら、その方が『ユニットテスト』が死ぬほど書きやすいからだ。ViewModelのテストにDB接続や非同期スレッドの心配なんてしたくないだろ?」

……見えた。

彼らが守ろうとしていたのは、C#の文法じゃない。

  • ピーター(ドイツ)が守りたいのは、「堅牢性(Robustness)」と「一貫性(Consistency)」。多国籍チームで品質を保つには、全員が同じ「規約」に従うことが絶対だと信じてる。
  • ラヴィ(インド)が守りたいのは、「テスト容易性(Testability)」と「責務分離(Separation of Concerns)」。設計が綺麗なら、誰がどこを触っても壊れにくいと信じてる。

俺がやっちまったのは、彼らが「血の歴史」から築き上げてきた、この「文化的規約(カルチュラル・コード)」を、パフォーマンス改善という大義名分で土足で踏み荒らす行為だったんだ。


俺が編み出した「多国籍コードレビュー」の人生術 3ヶ条

この一件で、俺はC#のasync/awaitの仕組みをググり直すより、はるかに大事な「異文化コミュニケーション術」を学んだ。

それを、今まさに海外チームで消耗してるかもしれない君に、3つのルールとして授けたい。

ルール1:PRは「完成品(ドヤ顔)」を出すな。「叩き台(不安顔)」で出せ。

これ、マジで大事。

日本人は真面目だから、完璧なコードを書いてから「レビューお願いします(ドヤァ)」ってやりがち。俺もそうだった。

でも、それが一番炎上する。だって、レビューする側からしたら「もう完成しちゃってる」から、根本的な設計ミスを指摘しづらい。指摘したら「手戻り」がデカすぎる。だから、ピーターみたいに “fundamentally wrong” っていう強い言葉で止めるしかなくなる。

解決策:GitHubの「Draft Pull Request」を使い倒せ。

WIP(Work In Progress)でもいい。

「ピーター、ViewModelの非同期処理、AsyncDelegateCommand を使おうとしてるんだけど、この使い方で合ってるか不安だから、ちょっと見てもらえない?」

って、**不安な状態で(Draftで)**PRを投げるんだ。

さらに、PRの Description(説明文) が超重要。

ここに、以下の3点を「絶対に」書く。

  1. What(何を達成したか): 「DataGridのパフォーマンスを改善した」
  2. Why(なぜこの実装を選んだか): 「async/await と AsyncDelegateCommand を使い、UIスレッドをブロックしないようにした」
  3. Where I’m not sure(どこに不安・懸念があるか): 「AsyncDelegateCommand の例外ハンドリングが、チームの規約に沿ってるか自信がない。あと、IValueConverter のキャッシュ戦略は、これで十分か意見が欲しい」

これを書くだけで、「コイツ、わかってるな」感が爆上がりする。

レビューする側も、「ああ、ここを重点的に見ればいいんだな」と分かるから、精神的な負担が激減する。

「ドヤ顔PR」は敵を作る。「不安顔PR」は仲間を作る。

ルール2:「指摘(Fact)」と「人格攻撃(Fiction)」を切り離せ。

「This is wrong.(これ間違ってる)」

「This is inefficient.(これ非効率)」

「I don’t understand this code.(このコード意味わからん)」

海外エンジニア、特にヨーロッパ系は、こういうストレート(というか日本人からすると失礼)な表現を平気で使う。

言われた瞬間、カチンとくる。

「俺のコードが意味わからん、だと? お前の読解力がないだけじゃねーか?」って。

でも、待ってほしい。

彼らの99%は、君を人格攻撃してるわけじゃない。

彼らにとってそれは「技術的な事実(ファクト)」を述べてるだけなんだ。

彼らの文化では、「間違っている」ことを「間違っている」と指摘するのは、チームの品質を守るための「義務」であり「誠実さ」の証。

逆に、気を使って何も言わないのは「無責任」だとさえ思ってる。

だから、カッとならずに、その「指摘(Fact)」の裏にある「論理(Logic)」だけを引き出すことに集中する。

「OK、意味わからんのはわかった。どの部分が一番読みにくい? もし君なら、どうリファクタリングする?」

常に、冷静に、ロジカルに。感情で返したら、負けだ。

ルール3:「Emoji(絵文字)」と「GIF」を舐めるな。

ルール2と矛盾するようだけど、これが最強の「人生術」だ。

テキストだけのコミュニケーションは、マジで誤解を生む。

「OK.」の一言が、「(怒)わかったよ」なのか、「(喜)了解!」なのか、文化が違うとマジで伝わらない。

ここで「カルチュラル・コード・シンクロナイゼーション(CCS)」だ。

感情を、意図的に「同期」させるんだ。

  • 感謝を伝える時: :+1:(サムズアップ)だけじゃ足りない。「Thanks! This is super helpful! 🙏(ありがとう! めっちゃ助かった! 🙏)」
  • 指摘を理解した時:「Ah, I see! That makes perfect sense. 💡(なるほど、理解した! 💡)」
  • ヤバいミスを指摘された時:「Oops, good catch… 🤦‍♂️(うわ、マジだ… 🤦‍♂️)」
  • レビューで議論が白熱した時:「Hmm, let me think about that… 🤔(うーん、ちょっと考えさせて… 🤔)」
  • 無事にマージされた時:SlackやTeamsで、マージ通知と共に「Thanks for the review, team! 🚀(レビューあざす! 🚀)」とか、お祝いのGIF(ダンスしてる猫とか)を投下する。

バカみたいだろ?

でも、この「ワンクッション」があるだけで、レビューの「戦場感」がゼロになる。

「こいつ、ちゃんと俺の指摘をポジティブに受け止めてるな」

「こいつ、ユーモアわかるヤツだな」

この「感情の同期」こそが、多国籍チームで信頼を勝ち取る一番の近道なんだ。


あの炎上PRは、結局、ピーターやラヴィの指摘に従って、ViewModelをクリーンにし、AsyncDelegateCommand を使い、App.config を直し…と、ほぼ全面改修になった。

でも、俺は(感情的な)敗北感を一切感じなかった。

むしろ、PRがマージされた時、ピーターが最後にくれたコメントは、これだった。

“LGTM. Great discussion, welcome to the team.”

(いいね。素晴らしい議論だった。チームへようこそ)

コードレビューは、戦場じゃない。

文化や背景、設計思想の「ズレ」を同期(シンクロ)させるための、最高の「対話の場」なんだ。

…と、ここまでが「対話術」の話。

でも、この「対話(レビュー)」を毎回やってたら、正直疲れる。

ラヴィやピーターが言ってた「規約」や「設計思想」、そもそも「ルールがあるなら先に書いとけよ!」って話だろ?

その通り。

だから、次回は「書く」技術について話そう。

多国籍チームにおいて、いかに「読まれる」ドキュメント、アーキテクチャ図を残していくか。

俺がボコボコにされながら学んだ、「グローバル言語としてのドキュメント術」だ。

なぜ誰も読まない?アーキテクチャを「グローバル言語」に変えるドキュメント術。

(ここから本文:約3000文字)

ども!C#/WPFエンジニアとして、今日もヨーロッパの空の下でXAMLを書いてる俺だ。

前回の「承」では、あの炎上コードレビュー(async void と App.config ハードコード事件)を、いかにして鎮火し、対話を通じてチームの信頼を得るか、という「コミュニケーション術」について話した。

Draft PRとEmojiとGIFを駆使して(笑)、レビューを「戦場」から「対話の場」に変える。これはマジで効くから、まだやってないヤツは今すぐやるんだ。

で、俺は学んだ。

ピーター(ドイツ人アーキテクト)が守りたいのは「堅牢性」と「規約」。ラヴィ(インド人シニア)が守りたいのは「テスト容易性」と「責務分離」。

OK、OK、わかったよ。

「じゃあ、その『規約』と『設計思想』、先に書いとけよ!!」

そう、俺は叫びたかった。

いや、叫んだ。心の中で。

毎回毎回、コードレビューで「これはウチのやり方じゃない」って言われるのは、さすがに非効率すぎるだろ。

俺もバカじゃない。同じ轍は二度と踏まん。

次のタスクが来た時、俺は決意した。

「コードを書く前に、完璧なドキュメントを書いて、全員の合意を取ってやる」 と。

これが、俺の「第二の失敗」であり、このブログで一番伝えたい**「ドキュメントという名の新たなカルチュラル・コード」**の正体に気づいた瞬間だった。


完璧な「小説」を書いて、コオロギが鳴いた日

次に俺に回ってきたのは、そこそこデカいリファクタリングタスクだった。

WPFアプリによくある、設定(Settings)画面。これがもう、あらゆる設定が1つのViewModelにぶち込まれた「神クラス(God Class)」になってて、密結合の地獄と化してた。

「この設定を変えたら、あの画面のこの機能も動的にON/OFFしたい」みたいな要求が来るたびに、ViewModel同士がイベントを投げまくり、カオスが加速してる。

「はいはい、これ、MVVMのアンチパターンね」

俺は腕まくりした。

「こういうのは、EventAggregator とか Mediator パターン使って、ViewModel間の疎結合を促進すりゃいいんだろ。日本でもやったことあるぜ」

今回は失敗しない。

俺はコードを1行も書く前に、Wiki(Confluence)を開いた。

そして、俺の持てる力のすべてを注ぎ込んで、「設計ドキュメント」を書き始めた。

  • タイトル: 「設定モジュール・リファクタリング提案 – Mediatorパターン導入による疎結合化」
  • 1. 現状の問題点 (The Problem):
    • SettingsViewModel が肥大化しすぎてる。
    • ViewModel間の直接参照とイベントが複雑怪奇。
    • テストコードが書けない。
  • 2. 提案する解決策 (The Solution):
    • MediatR(.NETで有名なMediatorライブラリ)の導入。
    • Notification(通知)と Handler(処理)による疎結合なメッセージング。
  • 3. なぜ MediatR か (Why MediatR):
    • 他のパターン(EventAggregator等)との比較。
    • DIコンテナとの親和性。
  • 4. 実装例 (Code Example):
    • ISampleNotification の定義。
    • SampleHandler の実装。
  • 5. シーケンス図 (Sequence Diagram):
    • UMLのシーケンス図を使って、メッセージの流れを完璧に図解。

どうだ。

A4にして10枚はあろうかという、完璧なドキュメント。

これぞ日本の「様式美」。問題点、背景、解決策、代替案、実装例。

これ以上ないだろ。

俺は「ドヤ顔」でこのWikiのリンクをチームのSlackに投げた。

「@Peter @Ravi 、次の設定画面リファクタリングの設計案まとめたから、レビューしてくれ! これで進めようと思う👍」

……シーン。

1時間経っても、誰もリアクションしない。

3時間経っても、Emojiのひとつもつかない。

まるで、コオロギが鳴いてるみたいだった。

(あれ…? 忙しいのかな…?)

翌日のアーキテクチャ定例ミーティング。

俺は自信満々に「あのドキュメント、見てもらえた?」と切り出した。

ピーター(ドイツ人)は、眉間にシワを寄せながら俺のWikiを開き、マウスホイールをものすごい勢いでスクロールさせ、30秒後にこう言った。

“This is… a novel. I don’t have time to read a novel.”

(これは… 小説か。俺に小説を読んでる暇はない)

……は?

小説だと?

続けてラヴィ(インド人)が追い打ちをかける。

「(意訳)テキストが多すぎる。で、**『どのコンポーネントが、どのコンポーネントと、どう喋る(Talk)ように変わる』**んだ? その『境界(Boundary)』だけが知りたい」

俺は焦ってシーケンス図を見せた。

「こ、これを見てくれ! メッセージの流れはここに…」

ピーターは図を一瞥して、ため息をついた。

「これは『実装(Implementation)』だ。MediatR ライブラリの内部動作を説明されても困る。

俺が知りたいのは、**『アーキテクチャ(Architecture)』**だ。

お前の変更によって、既存のどのモジュールが影響を受ける? 新しい『ルール』はなんだ?」


C4モデルが俺を救った。「グローバル言語」の正体

ピーターはイライラしたようにMiro(オンラインホワイトボード)を開くと、たった4つの「箱」を描き始めた。

  1. [ SettingsView.xaml ] (WPF View)
  2. [ SettingsViewModel.cs ]
  3. [ IMediator ] (The Bus / Interface)
  4. [ OtherViewModels ] (e.g., MainViewModel, ToolbarViewModel…)

そして、彼は矢印を2本だけ引いた。

[ SettingsViewModel ] –(Sends)–> [ IMediator ]

[ IMediator ] –(Publishes)–> [ OtherViewModels ]

「いいか」とピーターは言った。

「俺たちが合意すべき『ルール』は、これだけだ。

  1. SettingsViewModel は、IMediator という『インターフェース』にだけ依存する。
  2. OtherViewModels も、IMediator にだけ依存する。
  3. SettingsViewModel は、OtherViewModels のことを一切知らなくていい(Must not know)

これだけだ。

お前がこの IMediator の実装に MediatR ライブラリを使おうが、自前で書こうが、正直どうでもいい。それは『実装』の話だ。

俺たち(チーム)が合意すべきなのは、この『境界(Boundary)』と『依存関係(Dependency)』だ」

……雷に打たれた。

俺が書いたA4 10枚の「小説」は、9割が「実装」の話だったんだ。

MediatR の使い方とか、シーケンス図とか。

でも、多国籍チームが知りたいのは、そんな「どうやって」の部分じゃなかった。

彼らが知りたいのは、「俺の仕事に影響があるか?」、つまり「アーキテクチャ上の『境界』と『規約』は何か?」だけだったんだ。

俺のドキュメントは、日本語の「行間を読め」「背景を察しろ」という文化(ハイコンテキスト)で書かれた「小説」だった。

彼らが求めるドキュメントは、「ルールだけを明確に示せ」という文化(ローコンテキスト)で書かれた「地図(マップ)」だったんだ。

この「ズレ」こそが、俺がぶち当たった「ドキュメントのカルチュラル・コード」だ。

「わかってるヤツ」になるためのドキュメント術 3ヶ条

この日を境に、俺は「小説家」を辞め、「地図製作者」になることを決意した。

俺がたどり着いた、「グローバル言語」としてのドキュメント術を共有する。

1. テキストは負債。図は資産。C4モデルを崇拝せよ。

まず、テキスト(文章)で説明しようとするな。テキストは「負債」だ。メンテナンスされなくなり、すぐに腐る。

「図(ダイアグラム)」こそが、エンジニアリングにおける「グローバル言語」であり「資産」だ。

じゃあどんな図を描くか?

俺が即採用したのは**「C4モデル(C4 Model)」**だ。

ググればすぐ出てくる。(※情報源として最後にリンクを貼る)

C4モデルは、アーキテクチャを4つの階層(Context, Containers, Components, Code)で表現する手法だ。

ピーターが描いた「4つの箱」は、まさにC4の「C3: コンポーネント図」レベルの話だった。

  • C1 (Context / コンテキスト図):システム全体と、外部のユーザーや他システムとの関係。
  • C2 (Containers / コンテナ図):システムを構成する大きな「実行可能な単位」(例: WPFアプリ、Web API、DB)。
  • C3 (Components / コンポーネント図):1つのコンテナ内部の主要な「部品」とその関係。(例: ViewModel, Service, Repository, そして IMediator!)
  • C4 (Code / コード図):(これはUMLとかでもいいけど)必要なら。

多国籍チームのアーキテクチャ議論で必要なのは、9割がC2とC3だ。

「俺たちのWPFアプリ(C2)は、どのAPI(C2)を呼ぶ?」

「このViewModel(C3)は、どのService(C3)に依存する?」

テキストで10ページ書く代わりに、draw.io や PlantUML でC3レベルの図を1枚描け。

それだけで、「コイツ、アーキテクチャわかってるな」って思われる。

2. ドキュメントは「Wiki」に置くな。「Git」に置け。

俺が書いた「小説」が読まれなかったもう一つの理由。

**「コードから遠い場所にあった」**からだ。

エンジニアが日常的に見るのは、IDE(Visual Studio)とGit(のリポジトリ)だ。Wikiなんて、よっぽど暇な時しか見ない。

解決策:ドキュメントは「Markdown(.md)」で書け。

そして、docs/architecture みたいなフォルダを作って、コードと一緒のGitリポジトリにコミットしろ。

C4の図も、PlantUML(テキストで図が書けるツール)を使えば、ただのテキストファイルだ。draw.io のファイルだってリポジトリに置ける。

3. ドキュメントは「コード」と一緒に「レビュー」させろ。

これが最強の「シンクロ術」だ。

ドキュメントをGitに置く最大のメリット。

アーキテクチャを変更する時、コードの変更と、「ARCHITECTURE.md の変更」を、同じPull Requestに含めるんだ。

俺は MediatR を導入する時、まず「docs/architecture/C3_SettingsModule.puml(PlantUMLファイル)」を修正するPRをDraftで投げた。

「ピーター、ラヴィ。設定モジュールのコンポーネント図をこう変えようと思うんだけど、この『境界(IMediator)』で合意できる?」

これで、彼らは「コード」じゃなくて「図」をレビューできる。

テキストの「小説」じゃなくて、「地図」の変更点だけをレビューできる。

ここで合意が取れれば、もうこっちのもんだ。

「OK、じゃあこの『地図』に従って『実装(コード)』を書くわ」

これで、手戻りがゼロになる。


「承」で学んだ「対話術(コミュニケーション)」

「転」で学んだ「地図(ドキュメンテーション)」

この2つを組み合わせたら、俺はもう怖いものナシだ。

…そう思ってた。

ピーターとラヴィのレビューは余裕でパスできるようになった。

だが、俺は忘れていた。

あの炎上PRで、もう一人、俺に指摘をくれた人物がいたことを。

そう、QAのアンナ(ポーランド人)だ。

「地図」も「対話」も完璧。さあ、PRをマージだ!

…その時、CI/CDパイプラインが真っ赤に染まった。

アンナから、あの時と同じメッセージが飛んできた。

「ごめん、あなたのブランチ、QA環境でまた動かないんだけど」

美しいアーキテクチャも、完璧なドキュメントも、動かなければ、ただのゴミだ。

俺たちが見落としていた最後のピース、それは「環境」だ。

次回、最終回。

「俺の環境じゃ動く」を地球上から撲滅する。

地球の裏側にいるQAでも一発で動く「ローカル環境ツイン」戦略について話そう。

「俺の環境じゃ動く」を撲滅せよ。地球の裏側でも動く「ローカル環境ツイン」戦略。

(ここから本文:約3000文字)

ども!ヨーロッパでC#/WPFと格闘中のエンジニアだ。

長かったこの話も、いよいよ最終回だ。

前回の「転」で、俺はついに「グローバル言語」を手に入れた。

A4 10枚の「小説」を捨て、C4モデルという「地図」をGitで管理する。コードレビューの前に、「地図」のレビューを終わらせることで、ピーター(ドイツ人アーキテクト)やラヴィ(インド人シニア)との設計思想の「ズレ」を、コードを書く前に同期(シンクロ)できるようになったんだ。

これで完璧だ。

「承」で学んだ「対話術」でレビューは円滑。

「転」で学んだ「ドキュメント術」で設計合意は爆速。

もう俺は、あの async void で炎上した半年前の俺じゃない。

「コイツ、わかってるな」って、チームでも一目置かれる存在に…

なれた、はずだった。

例の MediatR を導入したリファクタリングPR。

ピーターとラヴィのレビューは「LGTM!(Looks Good To Me!)」で一発OK。

俺はドヤ顔でマージボタンを押し、コードは自動的にCI/CDパイプラインに乗って、QA(テスト)環境にデプロイされた。

数時間後。俺のSlackに、あの女(ひと)からDMが来た。

QAのアンナ(ポーランド人)だ。

“Hey. Your build. It’s broken on QA again.”

(ねえ。あなたのビルド。またQA環境で動いてないんだけど)

……また?

デジャヴか?

俺は血の気が引くのを感じた。

「どんなエラーが出てる?」

「System.IO.FileNotFoundException: Could not load file or assembly ‘MediatR, Version=… って。あと、それが起きてる画面、DBの新しい設定値も読んでないみたいで、古い挙動のままだし」

FileNotFoundException? MediatR が見つからない?

そんなバカな。NuGetパッケージは csproj ファイル(WPFプロジェクトの定義ファイル)にちゃんと追加したぞ。

「俺のローカル環境じゃ、完璧に動いてる!」

………。

言っちまった。

世界中のエンジニアが、上司とQAから一番聞きたくないあのセリフ。

「俺の環境じゃ動く(It works on my machine)」

俺は愕然とした。

「対話」も「設計」も完璧にシンクロさせたのに、一番根本的な「動くモノ」がシンクロしてなかった。

ピーターが守りたかった「堅牢性」。

ラヴィが守りたかった「テスト容易性」。

そして、アンナが守りたかったのは、「テスト『可能』性(Testability)」…いや、もっとシンプルに**「動くこと(It works)」**だったんだ。

多国籍チーム、分散チームにおける最後の砦。

それが、**「環境(Environment)」**の同期だ。

俺の「カルチュラル・コード・シンクロナイゼーション(CCS)」には、この最後のピースが欠けていた。


なぜ「ズレ」はまた起きたのか

俺はアンナに平謝りし、調査に乗り出した。

原因は、驚くほど「しょうもない」ことだった。

  1. MediatR の件:CIサーバー(ビルドマシン)が、何らかの理由で nuget restore に失敗していた、あるいはキャッシュが古かった。ローカルでは入ってたけど、ビルド成果物(Installer)に含まれてなかった。
  2. DB設定値の件:俺は、新しい設定値を、自分のローカルの App.config には追記した。でも、QA環境用の App.config.Staging (ビルド時に置換されるべき設定ファイル)を更新し忘れていた。

最初の炎上PRでやらかした「App.config ハードコード事件」と、本質は同じだ。

俺は、**「自分のローカル環境」こそが「正義」**だと、無意識に思い込んでいた。

でも、分散チームにおいて、「俺のローカル環境」なんて、宇宙のチリほどの価値もない。

「CI/CDパイプラインが参照する環境」こそが、唯一の「正義」だ。

この問題を根本的に解決するには、俺のローカル環境を「正義」に合わせるしかない。

いや、「合わせる」じゃない。

俺のローカル環境を、CI/CDやQA環境と、寸分違わぬ「双子(ツイン)」にするんだ。

これが、俺が最終的にたどり着いた「ローカル環境ツイン戦略」だ。

WPFみたいなデスクトップアプリ開発でも、これは絶対にできる。


C#/WPF開発でも絶対やるべき「ローカル環境ツイン」戦略 3ヶ条

Web系(バックエンド)なら当たり前かもしれない。

でも、俺たちWPFエンジニアだって、もう「自分のPCにSQL Serverを手動インストール」してる時代じゃないんだ。

1. 「設定ファイル」を「環境変数」で上書き可能にしろ

App.config の configSource やXML変換(App.config.Staging)は、もう古い。事故の元だ。

イマドキのC#(.NET Core / .NET 5+はもちろん、.NET Frameworkでもライブラリ追加で)は、appsettings.json を使う。

  1. appsettings.json:全環境共通のデフォルト値(開発用DBの接続文字列とか)を書く。これはGitにコミットする。
  2. appsettings.CI.json / appsettings.Staging.json:各環境(CI用、QA用)の「上書き用」設定を書く。これもコミット。
  3. appsettings.Development.json:「俺のローカル専用」の上書き設定ファイル。これは、絶対に .gitignore に入れる。
  4. 環境変数:WPFアプリ起動時、IConfigurationBuilder が「appsettings.json → appsettings.{Environment}.json → 環境変数」の順で設定を読み込むようにコード(Program.cs や App.xaml.cs)を書く。

これが最強だ。

CI/CDパイプラインは、ビルド時に「Environment=Staging」という環境変数を設定するだけで、自動的に appsettings.Staging.json を読んでくれる。

俺たち開発者は、ローカルでだけ使いたいDBの接続文字列を .gitignore された appsettings.Development.json に書けば、うっかりコミットする事故がゼロになる。

2. データベースとAPIは「Docker」で起動しろ

「俺のローカルPCに、SQL Server 2019が…」

「俺のPCには、2017が…」

これがズレの温床だ。

WPFアプリ開発だろうが、関係ない。

依存する「外部サービス(DB、API)」は、すべてDockerで管理するんだ。

  • docker-compose.yml をリポジトリのルートに置け。

この docker-compose.yml に、

「mcr.microsoft.com/mssql/server:2019-latest(SQL Server 2019)のコンテナを起動しろ」

「初期データ(init.sql)も読み込ませろ」

と書く。

さらに、WPFアプリが依存してる「外部の決済API」とか「在庫API」があるだろ?

それも、docker-compose.yml で「モックサーバー(mountebank とか、簡単なASP.NET CoreのモックAPIとか)」を一緒に起動するように定義する。

新しくチームに入ったブラジル人エンジニアは、もう何もインストールする必要がない。

git clone して、docker-compose up とコマンドを一発叩くだけ。

それだけで、俺とも、アンナとも、CIサーバーとも**「まったく同じ」**DBとモックAPIが、彼のローカルPCに「双子」として爆誕する。

3. 「run.ps1」ですべてを起動可能にしろ

最後だ。

WPFアプリを起動するのに、Visual Studioで「ソリューション(.sln)を開いて、F5キーを押す」を必須にするな。

リポジトリのルートに、run.ps1(PowerShellスクリプト)か run.bat を置け。

このスクリプトがやることは、たった3つだ。

  1. docker-compose up -d (DBとモックAPIをバックグラウンドで起動)
  2. dotnet build (WPFアプリをビルド)
  3. ./bin/Debug/net6.0-windows/MyApp.exe (ビルドしたWPFアプリを実行)

git clone して、run.ps1 を実行すれば、アプリが起動する」

この状態を作れ。

なぜか?

CI/CDパイプラインも、QAのアンナも、まったく同じ run.ps1 を呼べばいいからだ。

もう「ビルド手順書.xlsx」なんていらない。

「俺の環境」と「CI環境」の手順がズレることもない。

run.ps1 という「実行可能な規約」が、すべての環境を「ツイン」として同期させるんだ。


結論:「シンクロ」こそが最強の「人生術」だ

長かった。

俺がこっちに来て、C#/WPFエンジニアとして学んだこと。

それは、async/await の使い方でも、MediatR の設計パターンでもなかった。

「カルチュラル・コード・シンクロナイゼーション(CCS)」

それは、俺の「当たり前」と、チームの「当たり前」の「ズレ」を、意図的に埋めていく作業だった。

  1. 【承】対話(Dialogue)のシンクロ:コードレビューは「戦場」じゃない。「対話」の場だ。Draft PRとEmojiで、感情と背景(コンテキスト)を同期させた。
  2. 【転】設計(Documentation)のシンクロ:ドキュメントは「小説」じゃない。「地図」だ。C4モデルとGitで、アーキテクチャという「規約」を同期させた。
  3. 【結】環境(Discipline)のシンクロ:「俺の環境」は「ゴミ」だ。「ツイン」こそが正義だ。Dockerとスクリプトで、実行「環境」という「規律」を同期させた。

技術力は、スタートラインに立つためのパスポートでしかない。

海外の多国籍チームで本当に「コイツ、できるな」と思われるエンジニアは、技術力(C#)でマウントを取るヤツじゃない。

「ズレ」に敏感で、その「ズレ」を埋めるための「仕組み(CCS)」を提案し、実行できるヤツだ。

これは、海外エンジニアリングに限った話じゃない。

リモートワークが進んだ日本のチームでも、きっと同じ「ズレ」が起きているはずだ。

このブログを読んでる君が、もし明日から海外で働くことになっても、もう大丈夫。

NullReferenceException は怖くない。

本当に怖いのは、文化や規約、環境の「NullReference(参照先のズレ)」だ。

でも、君はもう、その「ズレ」を「シンクロ」させる方法を知っている。

健闘を祈る。

俺もこっちで、まだまだシンクロし続けるぜ。

じゃあな!

コメント

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