はじめに

 C#の入門書を一通り読み終えたのに、どうにもモヤモヤが残る――そんな経験はありませんか。この連載では、C#のつまずきやすい概念を、生成AI(ChatGPTやGeminiなど)との対話を通じて攻略していきます。
 ポイントは、AIに「教えてもらう」姿勢です。わからないことを素直にぶつけ、深掘りし、コードを書かせ、検証する。この流れを身につければ、どんな概念でもAIを使って自力で学べるようになります。
 第1回のテーマは「if文地獄」。if文地獄がなぜよくないのか、改善する手段はどういうものか、AIを使って攻略していきます。

まずはコードを見せて「何が問題?」と聞く

 学習の出発点は、「このコード、何かまずい気がする」という違和感です。
 あなたは、コンビニのレジシステムを開発しています。最初はクレジットカードだけ対応すればよかったのですが、電子マネーやQRコード決済も、と要望が増えてきました。急いで対応したコードがこちらです。

public void ProcessPayment(string paymentType, decimal amount)
{
    if (paymentType == "CreditCard")
    {
        Console.WriteLine($"クレジットカードで {amount}円 を決済しました");
    }
    else if (paymentType == "ElectronicMoney")
    {
        Console.WriteLine($"電子マネーで {amount}円 を決済しました");
    }
    else if (paymentType == "QRCode")
    {
        Console.WriteLine($"QRコード決済で {amount}円 を決済しました");
    }
}

 素直なコードですが、このまま決済手段が増え続けたらどうなるでしょう?
 5種類、10種類と増えていくと、メソッドが肥大する気がします。この違和感を、AIにぶつけてみましょう。

AIに問題点を聞く

 次のプロンプトに続けて、先ほどのコードをそのまま入力してみます。その際、単に「問題点を教えて」と聞くと、多くの事柄が列挙され、どれが重要なのか見えづらくなりがちです。回答が広がりすぎたときは、数を絞ったり(ひとつだけ教えて)、観点を指定したり(保守する立場で困ることは?)すると効果的です。  ここでは最初から、もっとも問題になりそうなことと限定して聞いてみます。

次のC#コードを見てください。今は3種類の決済手段ですが、今後5種類、10種類と増えていく予定です。もっとも問題になりそうなことをひとつ教えてください。
(さきほどのコードをそのまま入力)

 こうすると、もっとも重要で本質的な回答が返ってきました(ChatGPTでの回答例です)。

もっとも問題になりそうなのは 「決済手段を追加するたびに、このメソッドを毎回編集しないといけなくなる(変更に弱い)」 ことです。

支払い手段が 5→10→… と増えるほど if / else if が伸びていき、修正箇所が常にここに集中します。
その結果、追加のたびに 既存の分岐を壊す/漏れが出る/テスト範囲が広がる という形で事故りやすくなります(以下、略)

 AIは、決済手段を追加するたびに、このメソッドを毎回編集しないといけなくなることが、もっとも問題だと指摘してくれました。これが、いわゆる「if文地獄」の正体です。
 では、どう直せばよいのでしょうか。さきほどの回答に、解決法が含まれているかもしれませんが、あらためて聞いてみます。

解決策を聞く

 回答がむやみに広がらないよう、指摘された問題に絞って、解決策を聞いてみましょう。

「決済手段を追加するたびに、このメソッドを毎回編集しないといけなくなる」という問題を解決するために、もっともよい方法を教えてください。

 ChatGPTでは、次のような回答でした。

いちばん筋がいいのは Strategyパターン(+DI)で「決済手段ごとの処理」をクラスに分離し、呼び出し側は"選ぶだけ"にする方法です。
こうすると 新しい決済手段を追加しても既存のProcessPaymentは基本的に触らないで済みます(OCP: 開放/閉鎖原則)。

例:インターフェース+実装クラス+レジストリ
(以下、コード例や「もっともよい」理由が続く)

 Geminiでも似たような内容で、インターフェースによる「ストラテジーパターン(Strategy Pattern)の導入」を提案してくれました。ただ、正直ピンと来ません。もう少し深掘りしてみましょう。

「もっとわかりやすく」と深掘りする

 技術用語が出てきて理解が止まったら、たとえ話や図解を求めるのが効果的です。

たとえ話を求める

 抽象的な概念を、身近な物にたとえてもらいます。

ストラテジーパターンがよくわかりません。身近な物にたとえて説明してもらえますか?

 Geminiでは、次のような回答でした(回答内容は、適宜要約しています)。

・「USB Type-C ポート」にたとえる
PC側は、挿されたものが何であるか詳しく知らなくても、「USB規格(インターフェース)に沿っている」ということさえ分かれば、通信を     開始できます。
ストラテジーパターンとは、「共通の接続口(インターフェース)」を作っておき、中身(具体的な処理)を後から「プラグイン」のように付け替え可能にする仕組みのことです。

 他にも、電源コンセントや決済端末など、いくつかのたとえを提案してくれました。最初のコードがまさに決済システムの話なので、もっとも身近な「決済端末」のたとえで図解を作ってもらいましょう。

図解を依頼する

 今回は、高品質の図解が得られる、Gemini 3 Pro Image(Nano Banana Pro)を使いました。Geminiのモデルで、「思考モード」か「Pro」を選択し、次のようなプロンプトを試してみます。

コンビニの「決済端末」のたとえを使って、ストラテジーパターンの概念を説明する図解をシンプルなイラストで作成してください。
  • Geminiの初回生成イメージ|

    Geminiの初回生成イメージ

 決済端末と複数の決済手段が描かれました。もちろんこれは一例で、AIはさまざまな図解を作成してくれるはずです。今回の図解だと、どれがインターフェースで、どれがクラスなのか、対応関係がわかりにくいですね。

不明点を確認する

 もう少し具体的に指示してみましょう。次のように聞いてみます。

この図で、「インターフェース」は具体的にどの部分に対応していますか? その部分をもっと強調してもらえますか?
  • 修正後のGeminiの生成イメージ

    修正後のGeminiの生成イメージ

 AIは「共通の差込口がインターフェースに対応する」と説明し、その部分を強調した図を生成してくれました。これで「インターフェース=共通の差込口」というイメージがつかめました。

「増えたらどうなる?」と聞く

 最初の問題は、「決済手段が増えるたびに修正が必要」でした。では、このたとえではどうなるのでしょうか?

新しく「顔認証決済」が追加されたら、この図はどう変わりますか?
  • 顔認証決済を追加したGeminiの生成イメージ

    顔認証決済を追加したGeminiの生成イメージ

 図を見ると、顔認証決済が追加されても、端末側はまったく変わっていません。新しい決済手段は、共通の差込口に合う形で接続されただけ。これがインターフェースの強みです。この、図解で拡張性を確認するステップは、後ほどコードでも検証します。  図解でイメージがつかめました。次は、これをコードで確認してみましょう。

「コードで書くとどうなる?」とAIに生成させる

 図解で理解できたら、次はコードを考えてみましょう。これまでの回答にコードが含まれているかもしれませんが、あらためてAIに依頼してみます。

コードを生成させる

 ここでもAIは、拡張性や安全性などを考慮したコードを作成する場合があります。それはそれで有用なものですが、より本質的なことを理解するために、最小限のコードで、といった指示を加えます。
 先ほどの、もっともよい方法を聞いた後なら、次のようになるでしょう。

その、もっともよい方法を用いて、最初の問題コードを改善してください。最小限のコードでお願いします。

 図解を生成した後なら、「図をコードにするならどうなりますか?最初の問題コードを~」というプロンプトでもいいでしょう。
 AIは次のようなコードを生成してくれました(実際の出力は異なる場合があります)。

// 共通のインターフェース(図の「共通の接続口」)
// 「この Pay機能 さえ持っていれば、どんなカードでも受け付けます」というルール
public interface IPayment
{
    void Pay(decimal amount);
}

// 各決済手段(インターフェースを実装するクラス)
public class CreditCard : IPayment
{
    public void Pay(decimal amount) => Console.WriteLine($"クレジットカードで {amount}円 を決済しました");
}

public class ElectronicMoney : IPayment
{
    public void Pay(decimal amount) => Console.WriteLine($"電子マネーで {amount}円 を決済しました");
}

public class QRCode : IPayment
{
    public void Pay(decimal amount) => Console.WriteLine($"QRコードで {amount}円 を決済しました");
}

// 決済端末(改善されたクラス)
// 以前のような if (paymentType == "CreditCard") ... という分岐は消滅しました。
public class ShopSystem
{
    public void ProcessPayment(IPayment paymentCard, decimal amount)
    {
        // 端末は、差し込まれたカードのボタンを押すだけ!
        // 中身がクレカなのかQRなのかは気にしません。
        paymentCard.Pay(amount);
    }
}

class Program
{
    static void Main()
    {
        ShopSystem terminal = new ShopSystem();

        // クレカを使いたいなら、クレカを作って渡す(差し込む)
        IPayment myCard = new CreditCard();
        terminal.ProcessPayment(myCard, 1000);

        // QRを使いたいなら、QRを作って渡す(差し込む)
        IPayment myPhone = new QRCode();
        terminal.ProcessPayment(myPhone, 500);
    }
}

 実行すると、次のように出力されます。

クレジットカードで 1000円 を決済しました
QRコードで 500円 を決済しました

 ShopSystemクラスは、渡された決済手段がクレジットカードなのかQRコードなのかを知りません。それでも、IPaymentインターフェースのPayメソッドを呼ぶだけで、正しい処理が実行されます。
 コードの構造を整理しておきましょう。まず、IPaymentインターフェースが「共通の差込口」に当たります。Payメソッドを持つことだけを定義しており、実装(メソッドの中身)はありません。
 CreditCard、ElectronicMoney、QRCodeの各クラスが、この差込口に合う「各決済手段」です。それぞれが、: IPaymentと書くことでIPaymentを実装し、Payメソッドの中身を具体的に書いています。「=>」は、1行で済むメソッドの省略記法です(中括弧で書いても同じ)。
 ShopSystemクラスが「決済端末」です。引数の型をIPaymentインターフェースにしているため、具体的な決済手段を知らなくても、Payメソッドを呼び出すだけで決済できます。

コードの意味を確認する

 コードは生成されましたが、なぜこれで問題が解決するのか、確認しておきましょう。

このコードで、最初の問題(決済手段が増えるたびに修正が必要)がどう解決されたのか、説明してください。

 AIの説明を要約すると、次のような内容でした。

  1. 「条件分岐(if文)」が消滅した
  2. 具体的な処理が「外」に出た
  3. 「変更」ではなく「追加」で対応できるようになった
 最初のコードでは、ProcessPaymentメソッドの中に、すべての決済手段の処理が詰め込まれていました。改善後は、各決済手段の処理がそれぞれのクラスに分離され、ShopSystemクラスはIPaymentインターフェースだけを相手にしています。先ほどの図解と同じことを、コードで表現できていますね。

顔認証決済を追加して検証する

 図解のときと同様に、本当に修正不要か、コードでも試してみましょう。

顔認証決済を追加するコードを書いてください。

 この回答では、次のようなクラスを追加するだけ、という回答になりました。

// 顔認証決済を追加(新しいクラスを作るだけ)
public class FaceAuthPayment : IPayment
{
    public void Pay(decimal amount) => Console.WriteLine($"顔認証で {amount}円 を決済しました");
}

 新しいクラスを1つ作るだけで、ShopSystemクラスには一切手を加えていません。使うときは、Mainメソッドに次のように書くだけです。

IPayment myFace = new FaceAuthPayment();
terminal.ProcessPayment(myFace, 2000);

 最初のif文地獄と比べてみましょう。

  • 改善前:決済手段を追加するたびに、ProcessPaymentメソッドを編集する(既存コードに手を入れる)
  • 改善後:新しいクラスを追加するだけ(既存コードには触れない)
 既存コードの変更が必要だったのが、新しいコードの追加になりました。触るコードの範囲がまったく違いますね。

「本当に解決した?」と疑問をぶつける

 ここで1つ疑問が浮かびました。AIに聞いてみます。

でも結局、どのIPayment実装クラスを使うか選ぶときに、if文が必要になりませんか? 本当にこれで問題は解決したのでしょうか?

 AIは、次のような言葉から回答を返してきました。

そのツッコミ、かなり核心です。結論から言うと、
「if文(分岐)をゼロにする」のが目的じゃなくて、「分岐を“増えない場所”に閉じ込める」のが目的です。
(以下、説明が続く)

 回答が長いので、「回答を抜粋する形で、要約して」と指示すると、次のような回答でした。

・分岐は「ゼロにする」のが目的ではなく、増えない場所に閉じ込めるのが目的。
・元コードは決済手段追加のたびに ProcessPayment を編集し、if/else が肥大化して壊れやすい。
・追加時にやるのは 新クラス追加 + 登録(設定)。既存の重要ロジックを触らずに済むので安全でテストもしやすい。
・「問題が解決した」とは、変更箇所を局所化して、追加が既存コード改造にならない状態にできた、という意味。

 AIは、if文は残るが、変更の影響範囲が限定されると説明してくれました。  確かに、ユーザーの選択に応じてどのクラスを生成するかを決める部分には、何らかの分岐が残ります。しかし、重要なのは、決済処理の本体であるShopSystemクラスからif文が消えたことです。if文をゼロにすることが目的ではなく、変更箇所を局所化して変更に強い設計にすることが目的というわけです。

「誰が選ぶの?」とさらに深掘りする  もう1つ疑問が出てきました。

ShopSystemクラスは具体的な決済クラスを知らないとのことですが、では、どの決済クラスを使うかは誰が決めるのですか?

 AIは、外部から注入するという考え方を説明してくれました。

決めるのはShopSystemではなく、外側の「構成担当」です。
具体的には、よくある選び方は3つあります。
1) DIコンテナが決める(いちばん多い)
2) 設定ファイル(appsettings等)が決める
3) Factory(工場役)が決める
(以下、具体的な説明が続く)

 先ほどのコードでも、Mainメソッドの中でIPaymentの実装クラスを生成し、ShopSystemのProcessPaymentメソッドに渡していました。これがまさに、外部から注入する形です。この考え方を発展させたものが、「依存性注入(DI)」と呼ばれる設計パターンです。興味がある方は、さらに深掘りしてみてください。

依存性注入が分かるように、先ほどの決済端末の図解を変更してもらえますか?
  • 依存性注入の視点を追加したGeminiの生成イメージ

    依存性注入の視点を追加したGeminiの生成イメージ

 また、依存性注入を使ったコード例を作成してもらうのもいいでしょう。  このように、1つの疑問から次の概念へと自然につながっていきます。今回学んだ、質問→深掘りの流れで、どんどん理解を広げていってください。

さらに学びたい方へ

 今回はif文地獄の解決策として、インターフェースを使ったストラテジーパターンを学びました。その先のテーマについても、AIと対話してみましょう。本文で触れた「依存性注入(DI)」は、誰が決済クラスを選ぶのか、という疑問を突き詰めた先にある設計パターンです。また、インターフェースと似たしくみに「抽象クラス」があります。どちらも共通の型を定義する点は同じですが、使いどころが異なります。  たとえば、抽象クラスとの違いは、次のように聞いてみてください。

C#のインターフェースと抽象クラスの違いを教えてください。今回の決済システムの例では、どちらを使うべきですか?

AIと対話するときのコツ

 今回の学習をふりかえりながら、対話のコツを整理してみましょう。

STEP1:違和感をぶつける

 「このコード、何が問題?」と聞く。回答が広がりすぎたときは、数を絞る(ひとつだけ教えて)、観点を指定する(保守する立場で困ることは?)、構造化させる(重要度順に3つ、一言ずつ)といった工夫が有効です。

STEP2:解決策を聞く

 「どう直せばいい?」と聞く。STEP1と同様、選択肢が多すぎるときは、もっともよい方法を1つと絞ると迷わない。知らない概念が出てきても、まずは受け止める。

STEP3:わからなければ深掘りする

 「もっとわかりやすく」「身近な物にたとえて」「図で説明して」と聞く。

STEP4:コードに落とす

 「コードで書くとどうなる?」と聞く。学習段階では、最小限のコード、もっともシンプルなコードで、などと添えると、本質と関係ない要素が省かれ、理解しやすくなる。

STEP5:検証する

 「本当にこれで解決する?」「なぜ?」と聞く。生成されたコードは実際に動かして確認する。

補足:AIの回答を鵜呑みにしない

 AIの回答は常に正しいとは限りません。動作するコードであっても、業務のプロジェクトで推奨されるスタイルと異なる場合もあります。「本当にこれで正しいですか?」「他の書き方はありますか?」と聞き返すことも大切です。

おわりに

 今回は、AIとの対話を通じて、if文地獄の解決策としてインターフェースを使ったストラテジーパターンを学びました。「質問→深掘り→コード生成→検証」の流れは、どんな概念を学ぶときにも使えます。ぜひ、ご自身でAIと対話しながら試してみてください。次回は、async/awaitを取り上げます。画面が固まる問題を、AIとの対話で解消していきましょう。

WINGSプロジェクト 髙江 賢(著)山田祥寛(監修)
有限会社 WINGSプロジェクトが運営する、テクニカル執筆コミュニティ(代表山田祥寛)。主にWeb開発分野の書籍/記事執筆、翻訳、講演等を幅広く手がける。現在も執筆メンバーを募集中。興味のある方は、どしどし応募頂きたい。著書、記事多数。
RSS
X:@WingsPro_info(公式)@WingsPro_info/wings(メンバーリスト)
Facebook

<著者について>
パソコン黎明期からプログラミングの進化を追い、Web・モバイル・IoT・AIまで多様な開発現場を駆け抜ける。
現在、株式会社気象工学研究所で気象×ITの最前線に立ちつつ、執筆コミュニティ『WINGSプロジェクト』のメンバーとして活動中。