Ответ 1
Обновление. Я переделал свой ответ, потому что после полуночного сна я действительно не чувствовал, что мой предыдущий ответ был очень хорошим.
Чтобы увидеть пример действия 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
. Эй, игры легкие!
Сфокусируйтесь на части 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
? Наверное, нет.
Но посмотрим, что произойдет, когда мы начнем добавлять дополнительные функции - скажем, из типичной RPG эпохи 1990-х:
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
, исключив все ненужные зависимости и несвязанные функциональные возможности. Если мы хотим что-то изменить в отношении того, как рассчитывается ущерб, очевидно, где искать.
Я надеюсь, что теперь это лучше объяснит принцип.