Понимание принципа единой ответственности дизайна SOLID

В настоящее время я пытаюсь изучить принципы проектирования SOLID вместе с разработкой, основанной на поведении, но мне довольно трудно понять принцип единой ответственности. Я пытался найти хороший учебник по С#, в котором используется разработка через тестирование, но не смог найти ничего стоящего в этом ключе. Как бы то ни было, потратив несколько дней на чтение, я решил, что лучший способ учиться — на собственном опыте, поэтому я начал создавать небольшое приложение, максимально используя эти принципы.

Это простой калькулятор очков в боулинге. Я решил, что лучше всего работать с самой простой частью, поэтому я начал с уровня мяча (или броска). Прямо сейчас я создал тесты, а также интерфейс и класс для работы с баллами мяча (броска), который гарантирует, что они не являются недействительными, т.е. <10 или >0. После реализации этого я понял, что класс ball - это просто целое число, допускающее значение NULL, поэтому, возможно, оно мне даже не нужно... но пока оно есть.

Следуя за мячом, я решил, что следующей логичной вещью, которую нужно добавить, будет интерфейс фрейма и класс. Здесь я застрял. Вот где я нахожусь:

namespace BowlingCalc.Frames
{
    public interface IFrame : BowlingCalc.Generic.ICheckValid
    {
        void AddThrow(int? score);
        int? GetThrow(int throw_number);
    }
}

namespace BowlingCalc.Frames
{
    public class Frame : IFrame
    {
        private List<Balls.IBall> balls;
        private Balls.IBall ball;
        private int frame_number;

        public Frame(Balls.IBall ball, int frame_number)
        {
            this.ball = ball;
            this.frame_number = frame_number;
            balls = new List<Balls.IBall>();
            check_valid();
        }

        public void AddThrow(int? score)
        {
            var current_ball = ball;
            current_ball.Score = score;
            balls.Add(current_ball);
        }

        public int? GetThrow(int throw_number)
        {
            return balls[throw_number].Score;
        }

        public void check_valid()
        {
            if (frame_number < 0 || frame_number > 10)
            {
                throw (new Exception("InvalidFrameNumberException"));
            }
        }

    }
}

Фрейм использует мой ранее реализованный класс мяча посредством внедрения зависимостей, чтобы добавить баллы мяча в кадр. Он также реализует способ возврата счета для любого заданного мяча в кадре.

Что я хочу сделать и где я застрял, так это добавить способ убедиться, что оценка кадра действительна. (На данный момент только простой случай кадров 1-9, где суммарный счет обоих шаров должен быть 10 или меньше. Я перейду к гораздо более сложному случаю кадра 10 позже.)

Проблема в том, что я понятия не имею, как реализовать это ТВЕРДЫМ способом. Я думал добавить логику в класс, но это, похоже, противоречит принципу единой ответственности. Затем я подумал добавить его как отдельный интерфейс/класс, а затем вызывать этот класс в каждом кадре, когда его оценка обновляется. Я также подумал о создании отдельного интерфейса, IValidator или что-то в этом роде, и реализации его в классе Frame. К сожалению, я понятия не имею, какой из них является лучшим/НАДЕЖНЫМ способом ведения дел.

Итак, что было бы хорошим, НАДЕЖНЫМ способом реализовать метод, который проверяет оценку кадра?


Примечание. Любая критика моего кода приветствуется. Я очень рад научиться создавать лучший код и рад получить любую помощь.


person Scott Lemmon    schedule 06.11.2012    source источник
comment
Это одно из лучших чтений о SOLID. В нем немного кода, но он поможет вам принять решение — adevait.com/software/   -  person Tosho Trajanov    schedule 30.10.2019


Ответы (2)


Какова цель интерфейса ICheckValid? Вы звоните check_valid в другое место? На мой взгляд, поскольку frame_number на самом деле является свойством Frame только для чтения, почему было бы неправильно проверять его согласованность прямо в конструкторе без каких-либо дополнительных интерфейсов для этого? (Предполагается, что конструкторы создают непротиворечивые объекты и поэтому могут свободно проверять входящие параметры по своему усмотрению.)

Однако вместо того, чтобы спрашивать, как правильно проверить это поле, было бы лучше спросить, зачем вам действительно нужно свойство frame_number в Frame? Похоже, это индекс этого элемента в каком-то массиве - можно просто использовать индекс, зачем хранить его в Frame? Позже вы можете написать некоторую логику if/else, например:

if (frame_number == 10) {
     // some rules
} else {
     // other rules
}

Однако вряд ли это НАДЕЖНЫЙ подход, поскольку вы, вероятно, в конечном итоге будете писать операторы if/else во многих частях файла Frame. Вместо этого вы можете создать базовый класс FrameBase, определить в нем большую часть логики, а также некоторые абстрактные методы, которые будут реализованы в OrdinaryFrame и TenthFrame, где вы определите разные правила. Это позволит вам полностью избежать frame_number — вы просто создадите девять OrdinaryFrames и один TenthFrame.

Что касается критики: ваш код, похоже, абстрагирует шары и рамки, но по какой-то причине игнорирует «броски» или «броски». Учтите необходимость добавления информации о траектории каждого броска, вам нужно будет изменить интерфейс IFrame, добавив что-то вроде void SetThrowTrajectory(int throwNumber, IThrowTrajectory trajectory). Однако, если вы абстрагируете отбросы, например. IBallRoll туда легко поместилась бы функциональность, связанная с траекторией (а также некоторые логические вычисляемые свойства, например, IsStrike, IsSpare).

person Andrew Sklyarevsky    schedule 06.11.2012
comment
Вау, спасибо! Это действительно много хороших советов, особенно об использовании базового класса фрейма и наследовании от него для создания обычного фрейма, а затем классов фреймов. Это замечательный совет. - person Scott Lemmon; 07.11.2012
comment
Кстати, для проверки кадра вы совершенно правы, номер кадра вообще не нужен, я определенно переосмыслил это. - person Scott Lemmon; 07.11.2012
comment
Что касается абстракции бросков. Прошу прощения, я понял, что мой класс шаров назван неподходящим образом. Его действительно следует называть броском или броском мяча, так как это его цель — отслеживать информацию о броске, а не информацию о мяче. Так что на самом деле это та абстракция, о которой вы говорили. - person Scott Lemmon; 07.11.2012
comment
Наконец, что я действительно хочу проверить в классе фреймов, так это то, что количество сбитых кеглей для двух бросков для нормального фрейма не превышает 10. Например, когда бросок 1 получает 4 кегли, 2 получает 5, кадр действителен, но когда бросок 1 получает 6, а затем бросок 2 получает 5, кадр вызывает исключение. Это то, что мне действительно трудно понять, как реализовать SOLID. - person Scott Lemmon; 07.11.2012
comment
Что касается ICheckValid, да, я использую его в плохо названном интерфейсе и классе IBall, чтобы проверить, действителен ли счет броска, например, между 0 и 10. Хотя с тех пор я понял, что как только я реализую проверку проверки, о которой я говорил выше, это вероятно, сделает проверку мяча/броска избыточной. - person Scott Lemmon; 07.11.2012
comment
Проблема, которую следует учитывать, заключается в следующем: когда вызывать проверку? Подход ICheckValid может привести к серьезным неуместным исключениям. Я бы рекомендовал генерировать исключение как можно раньше. Если оценка неуместна для добавления - тогда сразу же выбрасывайте исключение, зачем вам менять объект в неправильное состояние и ждать, пока какие-то внешние силы проведут проверку этого состояния? - person Andrew Sklyarevsky; 08.11.2012

Когда я думаю о SRP, я обычно делаю акцент на аспекте ответственности. Название класса, в свою очередь, должно в идеале описывать его Ответственность. Для некоторых классов это то, чем должен «быть» класс (хорошим примером может быть фрейм, если ему не хватает поведения и он просто представляет состояние), но когда у вас есть поведенческая ответственность, имя соответствует тому, чем должен быть класс. делать'.

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

//My job is to keep score; I don't interpret the scores
public interface IScoreKeeper : IScoreReporter
{
    void SetScore(int bowlerIndex, int frameIndex, int throwIndex, int score);
}

//My job is to report scores to those who want to know the score, but shouldn't be allowed to change it
public interface IScoreReporter
{
    int? GetScore(int bowlerIndex, int frameIndex, int throwIndex);        
}

//My job is to play the game when told that it's my turn
public interface IBowler
{
    //I'm given access to the ScoreReporter at some point, so I can use that to strategize
    //(more realisically, to either gloat or despair as applicable)

    //Throw one ball in the lane, however I choose to do so
    void Bowl(IBowlingLane lane);
}

//My job is to keep track of the pins and provide score feedback when they are knocked down
//I can be reset to allow bowling to continue
public interface IBowlingLane
{
    int? GetLastScore();

    void ResetLane();
}

//My job is to coordinate a game of bowling with multiple players
//I tell the Bowlers to Bowl, retrieve scores from the BowlingLane and keep
//the scores with the ScoreKeeper.
public interface IBowlingGameCoordinator
{
    //In reality, I would probably have other service dependencies, like a way to send feedback to a monitor
    //Basically anything that gets too complicated and can be encapsulated, I offload to some other service to deal with it
    //I'm lazy, so all I want to do is tell everybody else what to do.

    void PlayGame(IScoreKeeper gameScore, IEnumerable<IBowler> bowlers, IBowlingLane lane);
}

Обратите внимание: если вы хотите использовать эту модель для простого подсчета очков (без участия в реальной игре), у вас может быть заглушка Bowler (которая ничего не делает) и MockBowlingLane, который выдает ряд значений очков. BowlingGameCoordinator заботится о текущем боулер, фрейме и броске, поэтому очки накапливаются.

person Dan Bryant    schedule 07.11.2012
comment
Спасибо за отличный ответ. На данный момент для меня это немного высокий уровень. Прямо сейчас я просто хочу сосредоточиться на подсчете очков, но это дает мне много пищи для размышлений и хороший тест для расширения моего подсчета очков, когда я его закончу. - person Scott Lemmon; 08.11.2012