【问题标题】:Express type composition in OOPOOP 中的表达类型组合
【发布时间】:2020-03-01 14:53:50
【问题描述】:

假设我们要编写一个具有 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()}
}

但我不喜欢这种方法,因为任何时候我想创造一个圣骑士,我也必须创造一个法师和一个战士,基本上是同一个人

【问题讨论】:

  • 如果你可以在运行时将这些技术中的任何一种应用于任何角色呢?
  • c# 和 typescript 接口的工作方式不同,在 c# 中你必须明确声明你正在实现一个接口。您将不得不考虑不同的方法
  • @Stefan,我不确定你的意思。不需要技术的运行时组合
  • @Tal 为什么这种差异很重要?

标签: c# typescript oop design-patterns composition


【解决方案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();
    }

}

然后您可以开始构建它以包含更复杂的逻辑,例如与技能、冷却时间、技能选择等相关的成本等。

希望这会有所帮助!

【讨论】:

  • 你好阿尔伯特。谢谢您的回答 !您的解决方案中有一件事困扰着我:英雄类不知道能力的逻辑,它只知道能力是“可执行的”。假设我们要添加另一个可以治疗的基础类型“Healer”和可以治疗和射击的组合类“Druid”。在一种情况下你想进行愈合(),在另一种情况下你想射击()所以英雄类应该包含一些违反单一责任原则的选择逻辑
  • 酷。因此,我的示例中的功能基本上是封装状态和行为的命令,因此客户端对象不必“知道”任何有关使用它的能力的信息。把英雄简单地想象成一个门面。应推迟所有行为(如其他评论中所述转发),从而避免任何单一责任违规行为。我“强制”示例中的外观(英雄)具有选择行为以保持简短。
  • Albert,再次感谢您的回答!我有几个问题:为什么我们需要额外的分类等级(男性/魔法/远程)?如果我们添加不属于这些类别的能力怎么办?英雄类已经看起来像上帝的对象,我觉得它不会很好地扩展。我注意到的另一件事:如果能力的执行取决于每个能力所独有的某个参数集怎么办,在这种情况下,我们不能给他们接口 IHeroAbility 和多态调用能力。Execute() 不再可能
【解决方案2】:

您的Paladin 类对Composite design pattern 是封闭的,并且可以按原样转换为C#,也许在这里和那里有一些readonly 来改进它。

使用 Composition(此处为 Forwarding)被认为是一种比使用继承更强大的设计(请参阅 Composition over inheritance,但它可能感觉不那么自然,这就是为什么 IMO 你说:

但我不喜欢这种方法,因为任何时候我想创造一个圣骑士,我还必须创造一个法师和一个战士,这基本上是同一个人。

“同一个人”在语义上与继承的相关性高于组合,但会导致设计更加脆弱。从角色或能力的角度思考 (*) 更加灵活:HealerDruid 不是一个人,而是一个人/玩家/角色可以拥有的能力。

(*) 术语取自Albert's answer,但我仍然更喜欢您的解决方案更简单和明确。为了更进一步,我们需要一个参考场景列表和/或一个正在运行的原型(或任何现有程序)来评估设计和代码......

【讨论】:

  • 您好 Romain,感谢您抽出宝贵时间回答!看起来像您提到的通过“转发”进行组合是这种情况下的最佳解决方案。但我不明白“复合”部分。我认为我们在处理三分球时会使用 Composite,所以我在这里看不到连接
  • @OleksandrTe​​rletskyi :如果我理解您的问题,我们可以使用 Composite 让许多具有通用接口的对象看起来像具有该接口的对象。只需 2 个对象就足以将它们打包成一个组合。
【解决方案3】:

为此,我建议使用“装饰器设计模式”。

之后你可以让你的主角添加一些带有装饰器的特定行为。例如:

var  Fighter = new Charactermaker();
var  Mage  = new Charactermaker();
var  Archer = new Charactermaker();

KnifeDecorator addKnight = new KnifeDecorator(Fighter);
Paladin Decorator addPaladin= new PaladinDecorator(addKnight);

我有一些建议给你。您还可以使用“轻量级设计模式”来简化您的应用程序。它允许您通过在多个对象之间共享状态的公共部分而不是将所有数据保留在每个对象中,从而将更多对象放入可用的 RAM 量。

有关更多信息,您可以查看流动链接:

Decorator Design Pattern

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2019-07-27
    • 2017-07-09
    • 1970-01-01
    相关资源
    最近更新 更多