更新- 我重做了我的答案,因为经过半夜的睡眠,我真的不觉得我之前的答案很好。
要查看 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; }
}
好的,所以这将是一个非常无聊的角色扮演游戏。但是这个类是有道理的。这里的一切都与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);
}
……基本没用。它出什么问题了?
- 它没有明确的目的;通用的“行动”不是单一的责任;
- 它无法完成 a
Character
本身无法完成的任何事情;
- 它完全取决于,
Character
而不是别的;
- 它可能需要您公开
Character
您真正想要私有/受保护的部分类。
该类CharacterActions
打破了Character
封装,几乎没有添加任何自己的东西。DamageCalculator
另一方面,该类提供了新的封装,并通过消除所有不必要的依赖项和不相关的功能来帮助恢复原始类的凝聚力Character
。如果我们想改变计算伤害的方式,很明显应该去哪里看。
我希望这现在能更好地解释这个原理。