【デザインパターン】Compositeを学ぶ【C#】

C#

一言で言うと…

  • 単一オブジェクトとオブジェクトの集合(ツリー構造)を同じインターフェースで統一的に扱う仕組み

概要

Composite パターンは、GoF デザインパターンの一つで、構造に関するデザインパターンです。

オブジェクトをツリー構造に組み立て、「葉(Leaf)」と「枝(Composite)」を同じインターフェースで扱えるようにします。 クライアントは個別のオブジェクトと複合オブジェクトを区別せずに操作できます。

ゲーム開発では、単体魔法と複数の魔法を組み合わせたコンボ魔法を同じように扱いたい場面など、「単体」と「集合」を統一したいケースに適しています。

構成

classDiagram
    class IComponent {
        << interface >>
        + Execute()
    }
    class Leaf {
        - name : string
        + Execute()
    }
    class Composite {
        - name : string
        - children : List~IComponent~
        + Add(IComponent)
        + Remove(IComponent)
        + Execute()
    }
    Leaf ..|> IComponent : implements
    Composite ..|> IComponent : implements
    Composite o-- IComponent : contains
    Client ..> IComponent : << use >>
要素役割
IComponent葉と枝を統一するインターフェース
Leaf子を持たない末端オブジェクト
Composite子を持てる複合オブジェクト。自身も IComponent
ClientIComponent 経由でツリー全体を操作する

実装例(C#)

クラス概要

早速実装例です。今回はC#でゲーム開発を行う場合を想定してみました。

今回は攻撃魔法のコンボシステムを題材にします。

  • 単体魔法(FireBoltEffect など)は単独で発動できる
  • SpellCombo は複数の魔法をまとめて発動するコンボ魔法
  • SpellCombo の中にさらに SpellCombo を入れることで、ネストしたコンボも表現できる
炎・雷魔法コンボ(SpellCombo)
  ├─ 炎魔法コンボ(SpellCombo)
  │    ├─ FireBoltEffect(Leaf)
  │    └─ ExplosionEffect(Leaf)
  └─ ThunderEffect(Leaf)

ソースコード

1. 共通インターフェース(IComponent)

C#
/// <summary>
/// 魔法エフェクト共通インターフェース (IComponent)
/// 単体魔法もコンボ魔法も同じインターフェースで扱う
/// </summary>
public interface ISpellEffect
{
    string Name { get; }

    /// <summary>魔法を対象に発動する</summary>
    void Execute(Character target);
}

2. 葉オブジェクト(Leaf)

C#
using System;

/// <summary>火属性の単体魔法 (Leaf)</summary>
public class FireBoltEffect : ISpellEffect
{
    public string Name => "ファイアボルト";

    public void Execute(Character target)
    {
        int damage = 30;
        target.Hp -= damage;
        Console.WriteLine($"  {Name}:{target.Name}に{damage}の火属性ダメージ!");
    }
}

/// <summary>爆発の単体魔法 (Leaf)</summary>
public class ExplosionEffect : ISpellEffect
{
    public string Name => "エクスプロージョン";

    public void Execute(Character target)
    {
        int damage = 50;
        target.Hp -= damage;
        Console.WriteLine($"  {Name}:{target.Name}に{damage}の爆発ダメージ!");
    }
}

/// <summary>雷属性の単体魔法 (Leaf)</summary>
public class ThunderEffect : ISpellEffect
{
    public string Name => "サンダー";

    public void Execute(Character target)
    {
        int damage = 40;
        target.Hp -= damage;
        Console.WriteLine($"  {Name}:{target.Name}に{damage}の雷属性ダメージ!");
    }
}

3. 複合オブジェクト(Composite)

C#
using System;
using System.Collections.Generic;

/// <summary>
/// 複数の魔法を束ねるコンボ魔法 (Composite)
/// 子の ISpellEffect を保持でき、自身も ISpellEffect として振る舞う
/// </summary>
public class SpellCombo : ISpellEffect
{
    public string Name { get; }
    private readonly List<ISpellEffect> _effects = new List<ISpellEffect>();

    public SpellCombo(string name)
    {
        Name = name;
    }

    public void Add(ISpellEffect effect)    => _effects.Add(effect);
    public void Remove(ISpellEffect effect) => _effects.Remove(effect);

    /// <summary>束ねた魔法を順番に発動する(再帰的に委譲)</summary>
    public void Execute(Character target)
    {
        Console.WriteLine($"【{Name}】発動!");
        foreach (var effect in _effects)
        {
            effect.Execute(target);  // LeafでもCompositeでも同じ呼び出し
        }
    }
}

4. クライアントコード

C#
class CompositeSample
{
    static void Main()
    {
        var enemy = new Character("ドラゴン", hp: 500);

        // 単体魔法(Leaf)
        ISpellEffect fireBolt = new FireBoltEffect();

        // 炎魔法コンボ(Composite)= FireBolt + Explosion
        var fireCombo = new SpellCombo("炎魔法コンボ");
        fireCombo.Add(new FireBoltEffect());
        fireCombo.Add(new ExplosionEffect());

        // 炎・雷魔法コンボ(Composite)= 炎魔法コンボ(Composite)+ Thunder(Leaf)
        // CompositeがさらにCompositeを含む入れ子構造
        var fireThunderCombo = new SpellCombo("炎・雷魔法コンボ");
        fireThunderCombo.Add(fireCombo);
        fireThunderCombo.Add(new ThunderEffect());

        // 呼び出し側はLeafもCompositeも ISpellEffect として同じに扱う
        Console.WriteLine("--- 単体魔法 ---");
        fireBolt.Execute(enemy);
        // =>   ファイアボルト:ドラゴンに30の火属性ダメージ!

        Console.WriteLine("\n--- 炎魔法コンボ ---");
        fireCombo.Execute(enemy);
        // => 【炎魔法コンボ】発動!
        // =>   ファイアボルト:ドラゴンに30の火属性ダメージ!
        // =>   エクスプロージョン:ドラゴンに50の爆発ダメージ!

        Console.WriteLine("\n--- 炎・雷魔法コンボ ---");
        fireThunderCombo.Execute(enemy);
        // => 【炎・雷魔法コンボ】発動!
        // =>  【炎魔法コンボ】発動!           ← Compositeが再帰的に展開される
        // =>    ファイアボルト:ドラゴンに30の火属性ダメージ!
        // =>    エクスプロージョン:ドラゴンに50の爆発ダメージ!
        // =>   サンダー:ドラゴンに40の雷属性ダメージ!
    }
}

fireThunderCombo.Execute(enemy) の1回の呼び出しで、ネストしたコンボを含むツリー全体が再帰的に展開されます。

クライアントは FireBoltEffect(単体)と SpellCombo(コンボ)を区別せず、すべて ISpellEffect として扱えます。

使いどころ

  • ツリー構造でオブジェクトを表現したいとき(UIツリー、スキルツリー)
  • 単一オブジェクトと複合オブジェクトを統一的に操作したいとき
  • 再帰的な操作(全体への一括処理)を簡潔に書きたいとき

長所・短所

✅ 長所

  • 葉と枝を同じインターフェースで扱えるため、クライアントのコードがシンプルになる
  • ツリーへの追加・削除が容易で、新しいコンポーネントを自由に加えられる
  • 再帰的な操作(全体更新、全体実行など)が自然に書ける

⚠️ 短所

  • 葉と枝の共通インターフェースを設計するのが難しい場合がある
  • 制約が弱くなりがちで、特定の型の子しか持てないような制御がしにくい
  • 深いツリーでは再帰処理のパフォーマンスに注意が必要

他GoFパターンとの比較・関係

Iterator との関係

Composite のツリーを走査するために Iterator パターンを使うことがよくあります

ツリーを「どう歩くか(深さ優先・幅優先)」を Iterator に任せることができます。

Decorator との違い

Decorator も IComponent をラップしますが、子を持つ構造(ツリー)ではなく、単一オブジェクトへの機能追加が目的です。

Visitor との関係

Composite ツリーの各ノードに対して処理を適用するために Visitor パターンを使うことがあります。 ツリー構造の走査と処理を分離できます。

まとめ

  • Composite パターンは、葉と枝を同じインターフェースで統一してツリー構造を扱う
  • Execute() などのメソッドを葉・枝ともに持ち、再帰的に委譲することで一括操作が可能
  • ゲーム開発ではUIツリー・スキルツリーなどに適している
  • Iterator や Visitor と組み合わせるとツリー走査や操作がより柔軟になる
  • 葉と枝を区別せずに扱えることがクライアントコードのシンプルさにつながる
タイトルとURLをコピーしました