ウェブサイト検索

C# - OOPにおける型の合成を表現する


3つの基本的なキャラクタータイプ(ファイター、メイジ、アーチャー)を持つRPGゲームを書きたいとしましょう。また、ナイト、パラディン、レンジャーのような複合キャラクタータイプもあります。TypeScriptでは、キャラクターのインターフェースを次のように記述できます:

    // fighter can fight
    // mage can cast
    // archer can shoot
    // paradin can cast and fight
    // knight can fight and shoot
    // ranger can cast and shoot

    type Fighter = {fight: () => string}
    type Mage = {cast: () => string}
    type Archer = {shoot: () => string}
    type Knight = Fighter & Archer
    type Paladin = Fighter & Mage
    type Ranger = Archer & Mage

問題は、これらのインターフェースをOOPの技術だけを使ってどのように実装するかということです(C#のような主流のOOP言語で作業していると想像してください)。

私は、振る舞いを複製せずにこれを継承で表現することに成功しませんでした。現在、私が見ている唯一の方法は次の通りです:

class Paladin {
  constructor(private fighter: Fighter, private mage: Mage) {}

  fight() {this.fighter.fight()}
  cast() {this.mage.cast()}
}

でも私はその方法が好きではありません。なぜなら、パラディンを作りたいときにはいつでも、基本的に同じ人物であるメイジとファイターも作らなければならないからです。

回答 #1

もう少し編成を進めてみるといいかもしれません。実際には、異なる能力を持つ1種類だけで十分です。例えば:

public class Hero
{
    private readonly string name;
    private readonly IEnumerable<IHeroAbility> abilities;

    public Hero(string name, [Optional] IEnumerable<IHeroAbility> abilities)
    {
        this.name = name;
        this.abilities = abilities ?? new List<IHeroAbility> { new OhCrap() };
    }

    public void Attack()
    {
        Console.WriteLine(name);

        foreach (var ability in abilities) ability.Execute();

        Console.WriteLine();
    }
}

さあ、利用できる能力を準備してください:

public interface IHeroAbility
{
    void Execute();
}

public class SwingSwordAbility : IHeroAbility
{
    public void Execute()
    {
        Console.WriteLine("And with all his might the hero swung his sword down onto his opponent...");
    }
}

public class ShootArrowAbility : IHeroAbility
{
    public void Execute()
    {
        Console.WriteLine("The hero loads an arrow into his bow without blinking and launches it with fierce speed towards his opponant...");
    }
}

public class CastFireBallAbility : IHeroAbility
{
    public void Execute()
    {
        Console.WriteLine("The hero reaches deep into the underverse and hurls a ball of fire towards his opponant...");
    }
}

public class OhCrap : IHeroAbility
{
    public void Execute()
    {
        Console.WriteLine("The hero pats his pockets as if he forgot something at home...");
    }
}

それからあなたのヒーロータイプを作成してください。

class Program
{
    static void Main(string[] args)
    {
        IHeroAbility swingSwordAbility = new SwingSwordAbility();
        IHeroAbility shootArrowAbility = new ShootArrowAbility();
        IHeroAbility castFireBallAbility = new CastFireBallAbility();

        var fighter = new Hero("The Fighter", new[] { swingSwordAbility });
        fighter.Attack();

        var mage = new Hero("The Mage", abilities: new[] { castFireBallAbility });
        mage.Attack();

        var archer = new Hero("The Archer", abilities: new[] { shootArrowAbility });
        archer.Attack();

        var knight = new Hero("The Knight", abilities: new[] { swingSwordAbility, shootArrowAbility });
        knight.Attack();

        var paladin = new Hero("The Paladin", abilities: new[] { swingSwordAbility, castFireBallAbility });
        paladin.Attack();

        var ranger = new Hero("The Ranger",abilities: new[] { shootArrowAbility, castFireBallAbility });
        ranger.Attack();

    }
}

より明示的なアクショングループを望む場合は、Hero クラスを次のように変更することができます:

public class Hero
{
    private readonly string name;
    private readonly IEnumerable<IHeroAbility> meleeAbilities;
    private readonly IEnumerable<IHeroAbility> magicAbilities;
    private readonly IEnumerable<IHeroAbility> rangedAbilities;

    public Hero(
        string name,
        [Optional] IEnumerable<IHeroAbility> meleeAbilities,
        [Optional] IEnumerable<IHeroAbility> magicAbilities,
        [Optional] IEnumerable<IHeroAbility> rangedAbilities)
    {
        this.name = name;

        var defaultAbilities = new List<IHeroAbility> { new OhCrap() };
        this.meleeAbilities = meleeAbilities ?? defaultAbilities;
        this.magicAbilities = magicAbilities ?? defaultAbilities;
        this.rangedAbilities = rangedAbilities ?? defaultAbilities;
    }

    public void MeleeAttack()
    {
        var ability = meleeAbilities.First();
        Console.WriteLine(name);
        ability.Execute();
    }

    public void MagicAttack()
    {
        var ability = magicAbilities.First();
        Console.WriteLine(name);
        ability.Execute();
    }

    public void RangedAttack()
    {
        var ability = rangedAbilities.First();
        Console.WriteLine(name);
        ability.Execute();
    }

}

それから、能力に関連するコスト、クールダウン、能力の選択などのような、より洗練されたロジックを組み込むために、それを構築し始めることができます。

役に立てば嬉しいです!

回答 #2

あなたのPaladinクラスはCompositeデザインパターンに近く、必要に応じていくつかのreadonlyを加えることで少し改善できるかもしれませんが、そのままC#に翻訳できます。

ここでのフォワーディングによる コンポジション の使用は、継承を使用するものよりも強力なデザインと考えられています (継承よりコンポジションを参照) が、あまり自然に感じられない かもしれません。だからこそ、IMOではあなたがこう言ったのでしょう:

でも私はそのアプローチは好きではありません。なぜなら、パラディンを作りたいときはいつでも、ほぼ同じ人物であるメイジとファイターも作らなければならないからです。

「同じ人」は意味的には「継承」と「コンポジション」よりも関連が深いですが、より脆弱な設計につながります。役割や能力(*)の観点で考える方が柔軟です: 「ヒーラー」、「ドルイド」は人ではなく、人/プレイヤー/キャラクターが持つことのできる能力です。

(*)この用語はアルバートの回答から取ったものですが、私はそれでもあなたの解決策の方がずっと簡単で明確だと思います。さらに進めるには、設計とコードを評価するための参照シナリオのリストや、動作するプロトタイプ(または既存のプログラム)が必要です…