ボタンひとつでわかるポリモーフィズム ― Enemy/Slime/Goblinで学ぶ

2025年8月14日

対象:最短でポリモーフィズムを体感したい初学者

ねらい:まずは「ボタンを押すだけ」の最小実装で、“同じ呼び出しでも実体により結果が変わる”を確認。その直後に実務寄りの改善版へ進みます。


ゴール

  • 同じ型(Enemy)への同じ呼び出し(TakeDamage())でも、実体が Slime / Goblin で結果が変わることを体験する。

ソリューション名、プロジェクト名

PolySampleソリューション
PolySampleプロジェクト
で、作成し、GitHubDesktopで管理します


第1章:まずは基本アイデア(最小コード)

UI(WinForms)

  • ボタン3つ:
    • slime(テキスト:slime)…スライム選択
    • goblin(テキスト:goblin)…ゴブリン選択
    • button1(テキスト:こうげき)…攻撃実行 ※既定名のまま

コード(そのまま動く最小版)

using System;
using System.Windows.Forms;

namespace PolySample
{
    public partial class Form1 : Form
    {
        Enemy enemy;

        public Form1()
        {
            InitializeComponent();
        }

        private void slime_Click(object sender, EventArgs e)
        {
            enemy = new Slime();
        }

        private void goblin_Click(object sender, EventArgs e)
        {
            enemy = new Goblin();
        }

        private void button1_Click(object sender, EventArgs e) // 「こうげき」
        {
            enemy.TakeDamage();
        }
    }

    class Enemy
    {
        public virtual void TakeDamage()
        {
            Console.WriteLine("ダメージを受けた(ダミー)");
        }
    }

    class Slime : Enemy
    {
        public override void TakeDamage()
        {
            Console.WriteLine("スライムがダメージを受けた");
        }
    }

    class Goblin : Enemy
    {
        public override void TakeDamage()
        {
            Console.WriteLine("ゴブリンがダメージを受けた");
        }
    }
}

表示先の注意:WinForms 実行中の Console.WriteLine は[出力] ウィンドウに出ます(実行→[表示]→[出力]/上部ドロップダウン「デバッグ」)。

あえて残している課題

  • 敵未選択で攻撃すると NullReferenceException
  • 出力が画面上ではなく[出力]ウィンドウのみ。

第2章:学びやすさ&実務寄りに改善(命名を整える)

改善ポイント

  1. Null対策:敵未選択では攻撃不可(ボタン無効化&ガード)。
  2. UIでログ表示:ListBox にログを溜める。
  3. 抽象化の明確化:Enemy を abstract にして実装を子へ強制。
  4. 命名の整理:btnSlime, btnGoblin, btnAttack, lstLog, フィールドは _enemy。

追加UI

  • ListBox:lstLog
  • 起動時:btnAttack.Enabled = false

改善版コード(命名を統一)

using System;
using System.Windows.Forms;

namespace PolySample
{
    public partial class Form1 : Form
    {
        private Enemy _enemy;

        public Form1()
        {
            InitializeComponent();
            btnAttack.Enabled = false; // 敵が決まるまで攻撃不可
        }

        private void btnSlime_Click(object sender, EventArgs e)
        {
            _enemy = new Slime();
            Log("スライムがあらわれた!");
            btnAttack.Enabled = true;
        }

        private void btnGoblin_Click(object sender, EventArgs e)
        {
            _enemy = new Goblin();
            Log("ゴブリンがあらわれた!");
            btnAttack.Enabled = true;
        }

        private void btnAttack_Click(object sender, EventArgs e)
        {
            if (_enemy == null)
            {
                Log("先に敵を選んでください。");
                return;
            }

            string result = _enemy.TakeDamage(); // ポリモーフィズム
            Log(result);
        }

        private void Log(string message) => lstLog.Items.Add(message);
    }

    // 子クラスに実装を強制して意図を明確化
    public abstract class Enemy
    {
        public abstract string TakeDamage();
    }

    public class Slime : Enemy
    {
        public override string TakeDamage() => "スライムがダメージを受けた(ぷるぷる)";
    }

    public class Goblin : Enemy
    {
        public override string TakeDamage() => "ゴブリンがダメージを受けた(うめき声)";
    }
}

改善版で伝えられること

  • 安全性:未選択時の誤操作をUIで防ぐ/ガード節で二重に守る。
  • 拡張容易性:Enemy の派生を増やしても、呼び出し側は基本変更不要。
  • 責務分離:UIは「呼ぶだけ」、ふるまいはクラスが自分で持つ。

仕上げ:発展ミニ演習

  • Dragon : Enemy を追加し、ボタン1つ追加だけで動くことを確認。
  • Defend() を Enemy に追加し、各クラスで実装 → 画面に「ぼうぎょ」ボタンを追加。

了解です。前回の**改善版(命名を整えた完成形)**に合わせて、デザイナの配置図とモックのスクリーンショットを用意しました。最小版(button1の既定名のまま)についても配置図を併記します。

デザイナ配置図(最小版/button1は既定名)

コントロールNameText初期状態位置 (X,Y)サイズ (W,H)イベント
ButtonslimeslimeEnabled: True30, 70100, 32slime_Click
ButtongoblingoblinEnabled: True140, 70100, 32goblin_Click
Buttonbutton1こうげきEnabled: True250, 70100, 32button1_Click
FormForm1PolySampleStartPosition: CenterScreenClientSize: 680×420

備考:この最小版はログ表示コントロールなし。Console.WriteLine の出力は Visual Studio の[出力]ウィンドウ(表示→出力/ドロップダウンで「デバッグ」)に出ます。

デザイナ配置図(改善版/完成フォーム)

コントロールNameText初期状態位置 (X,Y)サイズ (W,H)アンカーイベント
ButtonbtnSlimeslimeEnabled: True30, 70100, 32Top,LeftbtnSlime_Click
ButtonbtnGoblingoblinEnabled: True140, 70110, 32Top,LeftbtnGoblin_Click
ButtonbtnAttackこうげきEnabled: False260, 70120, 32Top,LeftbtnAttack_Click
ListBoxlstLog(空)30, 120620〜740, 260〜300 目安Top,Bottom,Left,Right
FormForm1PolySampleStartPosition: CenterScreenClientSize: 800×450 目安

配置のコツ

  • btnAttack.Enabled = false を初期状態に。敵ボタンが押されたら true に切り替え。
  • lstLog は四辺アンカーでフォーム拡大に追従。
  • TabIndex は btnSlime:0 → btnGoblin:1 → btnAttack:2 → lstLog:3 を推奨。

完成フォーム「モック」スクリーンショット

注:環境に日本語フォントがない場合に備え、モック画像内のボタンラベルは ASCII で表記しています(例:btnAttack)。実際のフォームでは Text に日本語(こうげき など)を設定してください。


必要であれば、Form1.Designer.cs の InitializeComponent 完成版(座標・サイズ・アンカー・イベント配線まで含む)をそのまま貼り付けられる形でお渡しします。

訪問数 26 回, 今日の訪問数 1回