单一责任原则(SRP)和我的RPG的类结构看起来"怪异"

mik*_*dev 13 c# design-patterns solid-principles

我正在制作角色扮演游戏,只是为了好玩,并了解有关SOLID原则的更多信息.我关注的第一件事就是SRP.我有一个"角色"类,代表游戏中的角色.它有Name,Health,Mana,AbilityScores等等.

现在,通常我也会在我的Character类中放置方法,所以它看起来像这样......

   public class Character
   {
      public string Name { get; set; }
      public int Health { get; set; }
      public int Mana { get; set; }
      public Dictionary<AbilityScoreEnum, int>AbilityScores { get; set; }

      // base attack bonus depends on character level, attribute bonuses, etc
      public static void GetBaseAttackBonus();  
      public static int RollDamage();
      public static TakeDamage(int amount);
   }
Run Code Online (Sandbox Code Playgroud)

但是由于SRP,我决定将所有方法都移到一个单独的类中.我将该类命名为"CharacterActions",现在方法签名看起来像这样......

public class CharacterActions
{
    public static void GetBaseAttackBonus(Character character);
    public static int RollDamage(Character character);
    public static TakeDamage(Character character, int amount);
}
Run Code Online (Sandbox Code Playgroud)

请注意,我现在必须在我的所有CharacterActions方法中包含我正在使用的Character对象.这是利用SRP的正确方法吗?它似乎完全违背了封装的OOP概念.

或者我在这里做了一些完全错误的事情?

我喜欢这件事的一件事是我的Character类非常清楚它的作用,它只是代表一个Character对象.

Aar*_*ght 22

更新 - 我重新回答了我的答案,因为在半夜睡眠后,我真的不觉得我以前的答案非常好.

要查看SRP的实例,让我们考虑一个非常简单的角色:

public abstract class Character
{
    public virtual void Attack(Character target)
    {
        int damage = Random.Next(1, 20);
        target.TakeDamage(damage);
    }

    public virtual void TakeDamage(int damage)
    {
        HP -= damage;
        if (HP <= 0)
            Die();
    }

    protected virtual void Die()
    {
        // Doesn't matter what this method does right now
    }

    public int HP { get; private set; }
    public int MP { get; private set; }
    protected Random Random { get; private set; }
}
Run Code Online (Sandbox Code Playgroud)

好的,所以这将是一个非常无聊的RPG.但是这个课程很有意义.这里的一切都与此直接相关Character.每个方法都是由...执行或执行的操作Character.嘿,游戏很简单!

让我们专注于这个Attack部分,并尝试让这个有趣:

public abstract class Character
{
    public const int BaseHitChance = 30;

    public virtual void Attack(Character target)
    {
        int chanceToHit = Dexterity + BaseHitChance;
        int hitTest = Random.Next(100);
        if (hitTest < chanceToHit)
        {
            int damage = Strength * 2 + Weapon.DamageRating;
            target.TakeDamage(damage);
        }
    }

    public int Strength { get; private set; }
    public int Dexterity { get; private set; }
    public Weapon Weapon { get; set; }
}
Run Code Online (Sandbox Code Playgroud)

现在我们到了某个地方.角色有时会错过,并且伤害/命中随着等级上升(假设STR也增加).很高兴,但这仍然相当沉闷,因为它没有考虑目标的任何事情.让我们看看我们是否可以解决这个问题:

public void Attack(Character target)
{
    int chanceToHit = CalculateHitChance(target);
    int hitTest = Random.Next(100);
    if (hitTest < chanceToHit)
    {
        int damage = CalculateDamage(target);
        target.TakeDamage(damage);
    }
}

protected int CalculateHitChance(Character target)
{
    return Dexterity + BaseHitChance - target.Evade;
}

protected int CalculateDamage(Character target)
{
    return Strength * 2 + Weapon.DamageRating - target.Armor.ArmorRating -
        (target.Toughness / 2);
}
Run Code Online (Sandbox Code Playgroud)

在这一点上,问题应该已经在你的脑海中形成: 为什么Character负责计算自己对目标的伤害?为什么它甚至具备这种能力?对于这门课程的作用 有一些无形的奇怪,但在这一点上,它仍然有点含糊不清.仅仅将几行代码移出Character课堂真的值得重构吗?可能不是.

但是让我们来看看当我们开始添加更多功能时会发生什么 - 比如20世纪90年代典型的RPG:

protected int CalculateDamage(Character target)
{
    int baseDamage = Strength * 2 + Weapon.DamageRating;
    int armorReduction = target.Armor.ArmorRating;
    int physicalDamage = baseDamage - Math.Min(armorReduction, baseDamage);
    int pierceDamage = (int)(Weapon.PierceDamage / target.Armor.PierceResistance);
    int elementDamage = (int)(Weapon.ElementDamage /
        target.Armor.ElementResistance[Weapon.Element]);
    int netDamage = physicalDamage + pierceDamage + elementDamage;
    if (HP < (MaxHP * 0.1))
        netDamage *= DesperationMultiplier;
    if (Status.Berserk)
        netDamage *= BerserkMultiplier;
    if (Status.Weakened)
        netDamage *= WeakenedMultiplier;
    int randomDamage = Random.Next(netDamage / 2);
    return netDamage + randomDamage;
}
Run Code Online (Sandbox Code Playgroud)

这一切都很好,花花公子但是在Character课堂上做这个数字运算并不是有点荒谬吗?这是一个相当短的方法; 在一个真正的角色扮演游戏中,这种方法可以延伸到数百行中,具有豁免检定和所有其他方式的神经.想象一下,你带来了一个新的程序员,他们说:我得到了一个双击武器的请求,无论通常是什么样的伤害都应该加倍; 我需要在哪里进行更改? 你告诉他,检查Character课程. 咦?

更糟糕的是,也许游戏增加了一些新的皱纹,哦,我不知道,背刺奖金,或其他类型的环境奖金.那你到底该怎么想在Character课堂上解决这个问题呢?你可能最终会呼唤一些单身人士,比如:

protected int CalculateDamage(Character target)
{
    // ...
    int backstabBonus = Environment.Current.Battle.IsFlanking(this, target);
    // ...
}
Run Code Online (Sandbox Code Playgroud)

呸.这太糟糕了.测试和调试这将是一场噩梦.那么我们该怎么办?把它拿出Character课堂.这个Character班级应该知道如何做一个Character逻辑上知道如何做的事情,并且计算对目标的确切伤害实际上不是其中之一.我们将为它上课:

public class DamageCalculator
{
    public DamageCalculator()
    {
        this.Battle = new DefaultBattle();
        // Better: use an IoC container to figure this out.
    }

    public DamageCalculator(Battle battle)
    {
        this.Battle = battle;
    }

    public int GetDamage(Character source, Character target)
    {
        // ...
    }

    protected Battle Battle { get; private set; }
}
Run Code Online (Sandbox Code Playgroud)

好多了.这个课完全是一件事.它完成它在锡上的说法.我们已经摆脱了单例依赖,所以这个类实际上现在可以测试了,感觉更正确,不是吗?现在我们Character可以专注于Character行动:

public abstract class Character
{
    public virtual void Attack(Character target)
    {
        HitTest ht = new HitTest();
        if (ht.CanHit(this, target))
        {
            DamageCalculator dc = new DamageCalculator();
            int damage = dc.GetDamage(this, target);
            target.TakeDamage(damage);
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

即使是现在,一个人Character直接调用另一个Character人的TakeDamage方法也有点可疑,实际上你可能只是希望角色将其攻击"提交"到某种战斗引擎,但我认为这部分最好保留为锻炼给读者.


现在,希望你理解为什么这样:

public class CharacterActions
{
    public static void GetBaseAttackBonus(Character character);
    public static int RollDamage(Character character);
    public static TakeDamage(Character character, int amount);
}
Run Code Online (Sandbox Code Playgroud)

......基本没用.它出什么问题了?

  • 它没有明确的目的; 通用"行动"不是一项单一责任;
  • 它无法完成任何Character本身无法做到的事情;
  • 它完全取决于Character其他因素;
  • 它可能会要求您公开Character您真正想要私有/受保护的类的部分内容.

CharacterActions课间休息的Character封装,并增加了小到没有它自己.该DamageCalculator班,在另一方面,提供了一种新的封装,并有助于恢复原来的凝聚力Character,消除一切不必要的依赖和不相关的功能的类.如果我们想要改变计算损伤的方法,那么显而易见.

我希望现在能更好地解释这个原则.


Kal*_*son 7

SRP并不意味着一个类不应该有方法. 你所做的是创建一个数据结构而不是多态对象.这样做有好处,但在这种情况下可能没有意图或需要.

您通常可以判断对象是否违反SRP的一种方法是查看对象中方法使用的实例变量.如果有一组方法使用某些实例变量而不是其他实例变量,那通常表明您的对象可以根据实例变量组进行拆分.

此外,您可能不希望您的方法是静态的.您可能希望利用多态性 - 根据调用该方法的实例的类型,在您的方法中执行不同的操作的能力.

例如,如果你有a ElfCharacter和a ,你的方法是否需要改变WizardCharacter?如果你的方法绝对永远不会改变并完全自包含,那么静态就好了......但即便如此,它也会使测试变得更加困难.