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