更新 - 我重做了我的答案,因为睡了半夜之后,我真的不觉得我之前的答案很好。
要查看 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; }
}
好的,所以这将是一个非常无聊的 RPG。但是这个类有意义。这里的一切都与Character 直接相关。每个方法要么是由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; }
}
现在我们正在取得进展。角色有时会失误,并且伤害/命中会随着等级的增加而增加(假设 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);
}
此时,您的脑海中应该已经形成了一个问题:为什么Character 负责计算自己对目标的伤害?为什么它甚至有这种能力? 这个类在做什么有一些无形的奇怪,但在这一点上它仍然有点模棱两可。将几行代码移出Character 类真的值得重构吗?应该不会吧。
但让我们看看当我们开始添加更多功能时会发生什么——比如从典型的 1990 年代 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;
}
这一切都很好而且很花哨,但是在Character 课程中进行所有这些数字运算是不是有点荒谬?这是一个相当短的方法;在真正的 RPG 中,这种方法可能会扩展到数百行带有豁免和所有其他书呆子方式的行。想象一下,你请来了一个新程序员,他们说:我收到了一个双击武器的请求,它的伤害应该是正常情况下的两倍;我需要在哪里进行更改? 然后你告诉他,检查Character 类。 嗯??
更糟糕的是,也许游戏增加了一些新的皱纹,哦我不知道,背刺奖励或其他类型的环境奖励。那么你到底应该如何在Character 类中解决这个问题?您可能最终会调用一些单身人士,例如:
protected int CalculateDamage(Character target)
{
// ...
int backstabBonus = Environment.Current.Battle.IsFlanking(this, target);
// ...
}
哎呀。这太可怕了。测试和调试这将是一场噩梦。那么我们该怎么办?将其从 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; }
}
好多了。这个类只做一件事。它按照它在锡上说的做。我们已经摆脱了单例依赖,所以这个类现在实际上可以测试了,而且它感觉更正确,不是吗?现在我们的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);
}
}
}
即使是现在,一个 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);
}
...基本上没用。它有什么问题?
- 它没有明确的目的;通用“行动”不是单一责任;
- 它无法完成
Character 本身无法完成的任何事情;
- 这完全取决于
Character,仅此而已;
- 可能需要您公开您真正希望私有/受保护的
Character 类的部分内容。
CharacterActions 类打破了Character 封装,几乎没有添加任何自己的东西。另一方面,DamageCalculator 类提供了新的封装,并通过消除所有不必要的依赖项和不相关的功能来帮助恢复原始Character 类的内聚性。如果我们想改变计算伤害的方式,显而易见在哪里查看。
我希望这能更好地解释这个原理。