C# “我的rpg外观的单一责任原则(SRP)和类结构”;“奇怪”;
我正在做一个角色扮演游戏,只是为了好玩和了解更多关于坚实的原则。我首先关注的是SRP。我有一个代表游戏中角色的“角色”类。它有名字、生命值、法力、能力核心等 现在,通常我也会在我的角色类中放置方法,所以它看起来像这样C# “我的rpg外观的单一责任原则(SRP)和类结构”;“奇怪”;,c#,design-patterns,solid-principles,C#,Design Patterns,Solid Principles,我正在做一个角色扮演游戏,只是为了好玩和了解更多关于坚实的原则。我首先关注的是SRP。我有一个代表游戏中角色的“角色”类。它有名字、生命值、法力、能力核心等 现在,通常我也会在我的角色类中放置方法,所以它看起来像这样 public class Character { public string Name { get; set; } public int Health { get; set; } public int Mana { get; set;
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);
}
public class CharacterActions
{
public static void GetBaseAttackBonus(Character character);
public static int RollDamage(Character character);
public static TakeDamage(Character character, int amount);
}
请注意,我现在必须在所有CharacterActions方法中包含正在使用的角色对象。这是利用SRP的正确方式吗?这似乎完全违背了面向对象的封装概念
还是我在这里做错了什么
我喜欢的一点是,我的Character类非常清楚它的作用,它只是表示一个Character对象。更新-我重做了我的答案,因为经过半个晚上的睡眠,我真的觉得我以前的答案不是很好
为了看到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; }
}
现在我们有进展了。角色有时会失误,伤害/命中随着等级的增加而增加(假设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);
}
这一切都很好,但在角色
类中进行所有这些数字运算不是有点可笑吗?这是一个相当短的方法;在一个真实的RPG中,这个方法可以扩展到数百行,包括保存抛出和所有其他方式的nerditude。想象一下,你带来了一个新的程序员,他们说:我得到了一个双重打击武器的请求,该武器的伤害应该是正常情况下的两倍;我需要在哪里换车?然后你告诉他,检查字符类嗯??
更糟糕的是,也许游戏增加了一些新的皱纹,比如,哦,我不知道,背刺奖金,或者其他类型的环境奖金。那么在角色
类中,你他妈的该如何理解这一点呢?你最终可能会对一些单身汉喊叫,比如:
protected int CalculateDamage(Character target)
{
// ...
int backstabBonus = Environment.Current.Battle.IsFlanking(this, target);
// ...
}
恶心。这太糟糕了。测试和调试这将是一场噩梦。那我们该怎么办?将其从字符
类中取出。角色
类应该只知道如何做角色
在逻辑上应该知道如何做的事情,而计算对目标的确切伤害并不是其中之一。我们将为此制作一个课程:
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; }
}
好多了。这个类只做一件事。它照罐头上说的做。我们已经摆脱了单例依赖,所以这个类实际上现在可以测试了,而且感觉更正确,不是吗?现在我们的角色
可以专注于角色
动作:
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);
}
}
}
即使现在,一个角色
直接调用另一个角色
的TakeDamage
方法还是有点可疑,事实上,你可能只是想让角色“提交”攻击给某种战斗引擎,但我认为这部分最好留给读者作为练习
现在,希望您能理解为什么:
public class CharacterActions
{
public static void GetBaseAttackBonus(Character character);
public static int RollDamage(Character character);
public static TakeDamage(Character character, int amount);
}
…基本上是无用的。怎么了
- 它没有明确的目的;一般“行动”不是单一的责任李>
- 它无法完成
字符本身无法完成的任何事情李>
- 它完全取决于
字符
,而不是其他字符李>
- 它可能需要您公开真正需要私有/保护的
字符
类的部分
CharacterActions
类打破了字符
的封装,并且几乎没有添加任何内容。另一方面,DamageCalculator
类提供了一个新的封装,并通过消除所有不必要的依赖项和不相关的功能,帮助恢复原始Character
类的内聚性。如果我们想改变计算损失的方法,很明显我们应该去哪里寻找
我希望这能更好地解释这个原理。SRP并不意味着类不应该有方法。您所做的是创建一个数据结构,而不是创建一个多态对象。这样做是有好处的,但在这种情况下可能不是有意的,也不是必要的
判断对象是否违反SRP的一种方法是查看对象中的方法使用的实例变量。如果有一组方法使用某些实例变量,但没有使用其他实例变量,那么这通常表明可以根据实例变量组来拆分对象
另外,您可能不希望您的方法是静态的。您可能希望利用多态性——根据调用方法的实例的类型在方法中执行不同操作的能力
例如,如果您有ElfCharacter
和WizardCharacter
,您的方法是否需要更改?如果你的方法绝对不会改变,并且是完全自包含的,那么也许静态是可以的。。。但即使这样,测试也变得更加困难。我在设计类时使用的方法,也是OO所基于的方法,就是对象对真实世界的对象建模
让我们来看看角色。。。
设计一个角色类可能非常有趣。
可能是合同
鱼类
这意味着任何想要成为角色的东西都应该能够执行Walk()、Talk()、Attack(),并具有一些属性,如生命值、法力值
你可以有一个向导,一个有特殊属性的向导
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);
}
}
}
public class CharacterActions
{
public static void GetBaseAttackBonus(Character character);
public static int RollDamage(Character character);
public static TakeDamage(Character character, int amount);
}
public interface ICharacter
{
//...
IEnumerable<IAction> Actions { get; }
}
public interface IAction
{
ICharacter Character { get; }
void Execute();
}
public class BaseAttackBonus : IAction
{
public BaseAttackBonus(ICharacter character)
{
Character = character;
}
public ICharacter Character { get; private set; }
public void Execute()
{
// Get base attack bonus for character...
}
}