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,消除一切不必要的依赖和不相关的功能的类.如果我们想要改变计算损伤的方法,那么显而易见.
我希望现在能更好地解释这个原则.
SRP并不意味着一个类不应该有方法. 你所做的是创建一个数据结构而不是多态对象.这样做有好处,但在这种情况下可能没有意图或需要.
您通常可以判断对象是否违反SRP的一种方法是查看对象中方法使用的实例变量.如果有一组方法使用某些实例变量而不是其他实例变量,那通常表明您的对象可以根据实例变量组进行拆分.
此外,您可能不希望您的方法是静态的.您可能希望利用多态性 - 根据调用该方法的实例的类型,在您的方法中执行不同的操作的能力.
例如,如果你有a ElfCharacter和a ,你的方法是否需要改变WizardCharacter?如果你的方法绝对永远不会改变并完全自包含,那么静态就好了......但即便如此,它也会使测试变得更加困难.