Принцип единой ответственности (SRP) и структура классов моей RPG выглядят «странно»

Если он использует истинные пробелы вместо вкладок (как это звучит), нет, вы не можете отображать vim 2 пространства, где их нет 4. Однако вы можете сообщить vim следующим командам, чтобы заменить все 4-пространственные группы на и затем отобразите их как 2 пробела.

:set tabstop=4 ! display a tab as 4 columns
:set shiftwidth=4
:set noexpandtab
:gg=G   ! convert the whole file to tabs
:set tabstop=2 !display a tab as 2 columns
:set shiftwidth=2

Когда вы готовы отправить свою работу,

:set tabstop=4
:set shiftwidth=4
:set expandtab
:%retab

Должны преобразовать ее.

13
задан Péter Török 17 October 2010 в 13:57
поделиться

5 ответов

Вектор считается «безопасным для потоков», поскольку доступ к внутренним элементам вектора синхронизирован. Такие методы, как add () , get () , size () и т.д., синхронизированы таким образом, что изменения внутренней структуры Vector и доступ к этой внутренней структуре не могут быть обработаны одновременно отдельными потоками.

-121--3266152-

80386/i386 - первый 32-разрядный процессор Intel. Когда он был представлен, многие компиляторы начали использовать i386 в качестве флага, чтобы включить для него генерацию кода, имя для различных временных файлов и архитектурные номиналы в именах файлов. 80386 наследовали 486, 586 (он же Pentium) и остальные процессоры архитектуры x86. Но i386 застрял в качестве метки и используется в качестве псевдонима для x86 (так же, как amd используется в качестве псевдонима для архитектуры x64, даже если есть процессоры Intel x64).

-121--1582977-

Обновление - Я переделал свой ответ, потому что после полночного сна я действительно не чувствовал, что мой предыдущий ответ был очень хорошим.

Чтобы увидеть пример 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; }
}

OK, так что это будет довольно скучный RPG. Но этот класс имеет смысл . Все здесь напрямую связано с символом . Каждый метод является действием, выполняемым или выполняемым для Character . Эй, игры легкие!

Давайте сосредоточимся на части Атака и постараемся сделать так, чтобы это на полпути было интересно:

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 ? Вероятно, нет.

Но давайте посмотрим, что происходит, когда мы начинаем добавлять больше функций - скажем, из типичного 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;
}

Это все прекрасно и денди, но разве не немного смешно делать все это число-crunching в классе Символ ? И это довольно короткий метод; в реальной RPG этот метод может распространиться на сотни линий с сохранением бросков и всех других способов nerditude. Представьте себе, вы привлекаете нового программиста, и они говорят: я получил запрос на оружие с двойным поражением, которое должно удвоить все повреждения, которые обычно будут; где нужно внести изменения? И скажи ему: Проверьте класс Символ . Ха??

Еще хуже, может быть, игра добавляет какие-то новые морщины, как, о я не знаю, бонус за спину, или какой-то другой тип бонуса окружающей среды. Как, черт возьми, вы должны понять это в классе Character ? Вы, вероятно, закончите звонить в какой-то синглтон, как:

protected int CalculateDamage(Character target)
{
    // ...
    int backstabBonus = Environment.Current.Battle.IsFlanking(this, target);
    // ...
}

Yuck. Это ужасно. Тестирование и отладка будут кошмаром. Так что же нам делать? Извлеките его из класса Символ . Класс 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; }
}

Гораздо лучше. Этот класс делает ровно одно. Он делает то, что говорит на жестянке. Мы избавились от синглтон зависимости, так что этот класс на самом деле можно проверить сейчас, и он чувствует гораздо больше права, не так ли? И теперь наш Характер может сконцентрироваться на Характер действия:

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 разрывает инкапсуляцию Character и ничего не добавляет. С другой стороны, класс TrainsCalculator предоставляет новую инкапсуляцию и помогает восстановить когезию исходного класса Character путем устранения всех ненужных зависимостей и несвязанных функциональных возможностей. Если мы хотим изменить что-то о путь ущерб рассчитывается, очевидно , где искать.

Я надеюсь, что это объясняет принцип лучше сейчас.

22
ответ дан 1 December 2019 в 19:49
поделиться

Я думаю, что это зависит, могут ли поведение ваших персонажей может измениться. Например, если вы хотите, чтобы доступные действия были изменены (на основе чего-то другого, происходящего в RPG), вы можете пойти на что-то вроде:

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...
    }
}

Это позволяет вашему персонажу иметь столько действий, сколько вы хотите (означает, что вы можете добавить / удалить Действия без изменения класса символов), и каждое действие отвечает только за себя (но знает о характере) и для действий, которые имеют более сложные требования, наследуют от доступа к добавлению свойств и т. Д. Вы, вероятно, захотите другое возвращаемое значение для Выполните, и вы можете захотеть очередь действий, но вы получаете дрейф.

Примечание. Использование iCharacter вместо символа, поскольку символы могут иметь разные свойства и поведение (колдунок, волшебник и т. Д.), Но они, вероятно, все имеют действия.

Разделяя действия, он также облегчает тестирование намного проще, поскольку теперь вы можете протестировать каждое действие без необходимости провода полного символа, а с iCharacter вы можете создавать свой собственный (издеваться).

2
ответ дан 1 December 2019 в 19:49
поделиться

Я не знаю, что я действительно называю этот тип класса SRP в первую очередь. «Все, имеющие дело с Foo», обычно является признаком того, что вы не после SRP (который в порядке, это не подходит для всех типов дизайнов).

Хороший способ взглянуть на границы SRP: «Есть ли что-нибудь, я могу изменить о классе, который оставил бы большую часть класса Intact?» Если это так, отделите их. Или, чтобы поставить его другим способом, если вы касаетесь одного метода в классе, вы, вероятно, должны прикоснуться к ним дотронуться до всех. Одним из преимуществ SRP является то, что он минимизирует сферу того, что вы касаетесь, когда вы делаете изменение - если другой файл нетронут, вы знаете, что вы не добавили ошибки!

Классы символов, в частности, находятся на высоких степенях опасности стать классами Бога в RPG. Возможный способ избежать этого будет подходить к этому из другого способа - начать с вашего пользовательского интерфейса, а на каждом шаге просто утверждать, что интерфейсы, которые вы желаете существовать с точки зрения класса, который вы в настоящее время пишут существовал уже. Кроме того, посмотрите в инверсию принципа управления и насколько изменяется проектирование при использовании IOC (не обязательно контейнер IOC).

2
ответ дан 1 December 2019 в 19:49
поделиться

Способ, который я иду при проектировании классов, и который является то, что OO основано на том, что объект моделирует объект реального мира.

Давайте посмотрим на характер ... Проектирование класса персонажа может быть довольно интересным. Это может быть договор ICharacter Это означает, что все, что хочет быть персонажем, должен иметь возможность выполнять прогулку (), разговор (), атаку (), иметь некоторые свойства, такие как здоровье, мана.

Затем вы можете иметь волшебник, волшебник, имеющий особые свойства, а также, как он атаки отличается, чем, давайте скажем истребитель.

Я склонен не слишком принуждать принципами проектирования, но и думать о моделировании реального объекта мира.

0
ответ дан 1 December 2019 в 19:49
поделиться

SRP не означает, что класс не должен иметь методов. Вы создали структуру данных , а не полиморфный объект , который. В этом есть свои преимущества, но, скорее всего, в данном случае она не предназначена и не нужна.

Одним из способов часто определить, нарушает ли объект SRP, является просмотр переменных экземпляров, которые используют методы внутри объекта. Если есть группы методов, которые используют определенные переменные экземпляра, но не другие, то это обычно означает, что ваш объект может быть разделен на группы переменных экземпляра.

Кроме того, вы, вероятно, не хотите, чтобы ваши методы были статичными. Скорее всего, вы захотите использовать полиморфизм - возможность делать что-то другое внутри ваших методов в зависимости от типа экземпляра, на котором был вызван метод.

Например, нужно ли было бы изменить ваши методы, если бы у вас был ElfCharacter и WizardCharacter? Если Ваши методы никогда не изменятся и будут полностью автономны, то, возможно, статические методы в порядке... но даже в этом случае тестирование будет намного сложнее.

7
ответ дан 1 December 2019 в 19:49
поделиться
Другие вопросы по тегам:

Похожие вопросы: