Преимущества от принципов SOLID и их применение в тестировании

Главные преимущества от соблюдения принципов SOLID – сокращение расходов на программный продукт и повышение конкурентоспособности программного продукта и команды.

Каким образом достигается сокращение расходов и повышение конкурентоспособности?

  • Сокращение временных затрат на добавление нового функционала. Если вышел на рынок первым, то захватишь его целиком.
  • Сокращение убытков от ошибок в программном продукте, за счет повышения его качества.

Давайте быстро пробежимся по принципам SOLID сточки зрения бизнеса:

  1. Принцип единственной ответственности (The Single Responsibility Principle).
    Если код соответствует этому принципу, то его легче понять, отладить, изменить, и оттестировать. Можно делать более масштабные изменения, так как если что-то сломается, то это что-то будет только одной функцией, а не целой системой. К тому же, одну функцию легче покрыть тестами, и проверить, что после изменений ни чего не сломалось.
  2. Принцип открытости/закрытости (The Open Closed Principle).
    При соответствии кода этому принципу, добавление нового функционала потребует минимального изменения существующего кода. А значит, это снижение времени на рефакторинг.
  3. Принцип подстановки Барбары Лисков (The Liskov Substitution Principle).
    При соблюдении этого принципа, можно классами наследниками, заменять классы родителей, без переписывания другого кода. Соблюдение этого принципа облегчает соблюдение принципа открытости закрытости. В результате, экономия времени и денег.
  4. Принцип разделения интерфейса (The Interface Segregation Principle).
    Этот принцип перекликается с принципом единственной ответственности. Разбивая интерфейсы по назначению, мы не заставляем реализовывать не нужные функции в классах реализующих интерфейсы, значит будет меньше кода, что скажется на скорости реализации, и экономии денег.
  5. Принцип инверсии зависимостей (The Dependency Inversion Principle).
    Соблюдение принципа обеспечивает гибкость программы, и позволяет заменять одни классы, другими классами, при условии, что классы реализуют общий интерфейс. Это позволяет писать юнит тесты. Соблюдение этого принципа крайне желательно для написания тестируемого кода. А тестируемый код нужен для снижения репутационых рисков, облегчении рефакторинга, и экономии времени на отладке.

Предполагается, вы знакомы с принципами SOLID, поэтому они не будут описываться. К тому же есть масса статей с объяснениями принципов SOLID.

Далее речь пойдет о применении принципов SOLID, для снижения убытков от ошибок в программном продукте, за счет более удобного тестирования.

Рассмотрим код, который угадывает задуманное человеком число:

private void LegacyCode_Click(object sender, EventArgs e)
{
    _minNum = 1;
    _maxNum = 100;
    do
    {
        int medNum = (_minNum + _maxNum) / 2;
        var dr = MessageBox.Show($"это число больше {medNum} ?", "Вопрос", 
MessageBoxButtons.YesNo);
        if (dr == DialogResult.Yes)
            _minNum = medNum + 1;
        else
            _maxNum = medNum;

        if (_maxNum == _minNum)
        {
            MessageBox.Show($"Вы загадали {_minNum}!");
        }
    }
    while (_maxNum != _minNum);
}

Почему нельзя написать на функцию LegacyCode_Click юнит тест?

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

Как можно устранить зависимость выполнения кода от человека, или любой внешней системы, которую нежелательно встраивать в среду выполнения юнит тестирования?

Ответ: соблюсти принцип DIP (the Dependency Inversion Principle) принцип инверсии зависимости.

public void DoPlayGame()
{
    do
    {
        int medNum = (MinNum + MaxNum) / 2;
        var dr = _mesageBox.Show($"это число больше {medNum} ?", "Вопрос", MessageBoxButtons.YesNo);
        if (dr == DialogResult.Yes)
            MinNum = medNum + 1;
        else
            MaxNum = medNum;
        
if (MaxNum == MinNum)
        {
            _mesageBox.Show($"Вы загадали {MinNum} !");
        }
    }
    while (MaxNum != MinNum); 
};

Что изменилось в методе? «MessageBox» заменили на «_mesageBox». Но если «MessageBox» это статический класс, который взаимодействует с пользователем, то «_mesageBox» это свойство класса объявленное через интерфейс в классе:

public class GameDiMonolit
{
private IMessageBoxAdapter _mesageBox;
public int MinNum { get; set; }
public int MaxNum { get; set; }

public GameDiMonolit(IMessageBoxAdapter mesageBox)
{
    _mesageBox = mesageBox;
}

public void DoPlayGame()
{
    do
    {
        int medNum = (MinNum + MaxNum) / 2;
        var dr = _mesageBox.Show($"это число больше {medNum} ?", "Вопрос", MessageBoxButtons.YesNo);
        if (dr == DialogResult.Yes)
            MinNum = medNum + 1;
        else
            MaxNum = medNum;

        if (MaxNum == MinNum)
        {
            _mesageBox.Show($"Вы загадали {MinNum} !");
        }
    }
    while (MaxNum != MinNum); 
}
}
}

Объявление переменной «_mesageBox» с типом интерфейса и есть инверсия зависимости. При объявлении _mesageBox от конкретного класса, компилятор жестко связывает код с конкретным классом переменной, а объявление переменной от интерфейса дает свободу использовать любые экземпляры классов, которые реализуют интерфейс.

Для взаимодействия с пользователем нужно реализовать класс реализующий «IMessageBoxAdapter»:

public interface IMessageBoxAdapter
{
    DialogResult Show(string mes);
    DialogResult Show(string text, string caption, MessageBoxButtons buttons);
}

Реализуем его через паттерн адаптер:

public class MessageBoxAdapter : IMessageBoxAdapter
{
    public DialogResult Show(string mes)
    {
        return MessageBox.Show(mes);
    }
    public DialogResult Show(string text, string caption, MessageBoxButtons buttons)
    {
        return MessageBox.Show(text, caption, buttons);
    }
}

Вызов из формы будет, например таким:

private void btnDiInvCode_Click(object sender, EventArgs e)
{
    var _gameDiMonolit = new GameDiMonolit(new MessageBoxAdapter());
    _gameDiMonolit.MinNum = 1;
    _gameDiMonolit.MaxNum = 100;
    _gameDiMonolit.DoPlayGame();
}

Можно реализовывать различные «MessageBoxAdapter», которые смогут обращаться к разным классам, и метод «DoPlayGame» ни чего не будет об этом знать.

Давайте теперь рассмотрим, как можно тестировать метод «DoPlayGame». Какие есть сложности? Метод содержит цикл, а цикл может зациклится. К счастью в NUnit есть параметр «Timeout». Так как «DoPlayGame» внутри цикла содержит ветвления, это оператор if, и условие в while, то нужно как-то в тесте эмулировать нажатия на кнопки пользователя. При этом нажатия на кнопки должны быть продуманны на предмет того, чтобы все ветви кода были покрыты.

Для тестирования можно реализовать специализированный класс «MessageBoxList», который подставляет ответы пользователя из очереди:

public class MessageBoxList : IMessageBoxAdapter
{
    private Queue<DialogResult> _queueDialogResult;
    private List<string> _listCaption;
    private List<string> _listText;
        
    public List<string> ListCaption => _listCaption;
    public List<string> ListText => _listText;
    public Queue<DialogResult> QueueDialogResult => _queueDialogResult;

    public MessageBoxList()
    {
        _listText = new List<string>();
        _listCaption = new List<string>();
        _queueDialogResult = new Queue<DialogResult>();
    }
    public DialogResult Show(string text)
    {
        _listText.Add(text);
        return DialogResult.OK;
    }
    public DialogResult Show(string text, string caption, MessageBoxButtons buttons)
    {
        _listText.Add(text);
        _listCaption.Add(caption);
        return _queueDialogResult.Dequeue();
    }
}

Тогда тест будет таким:

[Test(),Timeout(5000)/*тестируемый метод может зациклится, предотвратим зависание лимитом на время исполнения*/]
public void DoPlayGameWithMessageBoxListTest()
{   //инициализация
    var messageBoxList = new MessageBoxList();
    var gameDiMonolit = new GameDiMonolit(messageBoxList);
    gameDiMonolit.MinNum = 10;
    gameDiMonolit.MaxNum = 40;
    messageBoxList.QueueDialogResult.Enqueue(DialogResult.Yes);
    messageBoxList.QueueDialogResult.Enqueue(DialogResult.Yes);
    messageBoxList.QueueDialogResult.Enqueue(DialogResult.No);
    messageBoxList.QueueDialogResult.Enqueue(DialogResult.No);
    messageBoxList.QueueDialogResult.Enqueue(DialogResult.Yes);

    //тестируемый метод
    gameDiMonolit.DoPlayGame();

    var etalonList = new List<string>()
    {
        "это число больше 25 ?",        "это число больше 33 ?",
        "это число больше 37 ?",        "это число больше 35 ?",
        "это число больше 34 ?",        "Вы загадали 35 !"
    };
    Assert.True(etalonList.SequenceEqual(messageBoxList.ListText), "Ошибка.");
}

Впрочем, специализированный класс для тестирования можно не писать, а пользоваться библиотекой Moq, в этом лучае, тест станет таким:

[Test(),
Timeout(5000)/*тестируемый метод может зациклится, предотвратим зависание лимитом на время исполнения*/]
public void DoPlayGameWithMoqTest()
{
    //инициализация
    var moqMessageBoxList = new Moq.Mock<IMessageBoxAdapter>();
    var gameDiMonolit = new GameDiMonolit(moqMessageBoxList.Object);

    moqMessageBoxList.Setup(a => a.Show("это число больше 25 ?", "Вопрос", 
    	MessageBoxButtons.YesNo)).Returns(DialogResult.Yes);
    moqMessageBoxList.Setup(a => a.Show("это число больше 33 ?", "Вопрос",  
    	MessageBoxButtons.YesNo)).Returns(DialogResult.Yes);
    moqMessageBoxList.Setup(a => a.Show("это число больше 37 ?", "Вопрос",  
    	MessageBoxButtons.YesNo)).Returns(DialogResult.No);
    moqMessageBoxList.Setup(a => a.Show("это число больше 35 ?", "Вопрос",  
    	MessageBoxButtons.YesNo)).Returns(DialogResult.No);
    moqMessageBoxList.Setup(a => a.Show("это число больше 34 ?", "Вопрос",  
    	MessageBoxButtons.YesNo)).Returns(DialogResult.Yes);

    gameDiMonolit.MinNum = 10;
    gameDiMonolit.MaxNum = 40;

    //тестируемый метод
    gameDiMonolit.DoPlayGame();

    moqMessageBoxList.Verify(a => a.Show("это число больше 25 ?", "Вопрос", MessageBoxButtons.YesNo), 
    	Moq.Times.Once);
    moqMessageBoxList.Verify(a => a.Show("это число больше 33 ?", "Вопрос", MessageBoxButtons.YesNo), 
    	Moq.Times.Once);
    moqMessageBoxList.Verify(a => a.Show("это число больше 37 ?", "Вопрос", MessageBoxButtons.YesNo), 
    	Moq.Times.Once);
    moqMessageBoxList.Verify(a => a.Show("это число больше 35 ?", "Вопрос", MessageBoxButtons.YesNo), 
    	Moq.Times.Once);
    moqMessageBoxList.Verify(a => a.Show("это число больше 34 ?", "Вопрос", MessageBoxButtons.YesNo), 
    	Moq.Times.Once);
    moqMessageBoxList.Verify(a => a.Show("Вы загадали 35 !"), Moq.Times.Once);
}

В чем принципиальная разница между тестом со специализированным классом «MessageBoxList», и тестом с использованием Moq?

Ответ: Метод со спец. Классом «MessageBoxList» дает больше гибкости, и он позволляет контролировать последовательность ответов тестируемого метода пользователю. Тест с использованием Moq, проверяет просто наличие ответов, но в какой последовательности они пришли, он не проверяет.

Как видим, для написания тестов пришлось немножко подумать, а тесты должны быть простыми, и писаться почти механически. Это вполне достижимо, если при написании кода соблюдать еще один принцип SOLID, который был нарушен, а именно единственной ответственности. Какие ответственности можно выделить в этом методе?

public void DoPlayGame()
{
    do
    {
        //ответственность:сообщения (обычные сообщения)
        int medNum = (MinNum + MaxNum) / 2;
        var dr = _mesageBox.Show($"это число больше {medNum} ?", "Вопрос", MessageBoxButtons.YesNo);
        if (dr == DialogResult.Yes)
            MinNum = medNum + 1;
        else
            MaxNum = medNum;

        //ответственность: сообщения (финальное сообщение)
        if (MaxNum == MinNum)
        {
            _mesageBox.Show($"Вы загадали {MinNum} !");
        }
    }
    while (MaxNum != MinNum); //ответственность: игровой цикл
}
}
}

Выделим ответственности в другие классы:

//ответственность: игровой цикл
public class GameCycle
{
    public IGameQuestion GameQuestion;
    private GameCycle() { }
    public GameCycle(IGameQuestion gameLogic)
    {
        GameQuestion = gameLogic;
    }
    public void Cycle()
    {
        while (!GameQuestion.RegularQuestion());
    }
}
//ответственность: Сообщения
public interface IGameQuestion
{
    int MaxNum { get; set; }
    int MinNum { get; set; }

    bool RegularQuestion();
    bool FinalQuestion();
}
//ответственность: Сообщения
public class GameQuestion : IGameQuestion
{
    IMessageBoxAdapter _mesageBox;
    private GameQuestion() { }
    public int MinNum { get; set; }
    public int MaxNum { get; set; }

    public GameQuestion(IMessageBoxAdapter mesageBox)
    {
        _mesageBox = mesageBox;
    }

    //обычные сообщения
    public bool RegularQuestion()
    {
        int medNum = (MinNum + MaxNum) / 2;
        var dr = _mesageBox.Show($"это число больше {medNum} ?", "Вопрос", MessageBoxButtons.YesNo);
        if (dr == DialogResult.Yes)
            MinNum = medNum + 1;
        else
            MaxNum = medNum;

        bool res = FinalQuestion();
            
        return res;
    }

    //финальное сообщение
    public bool FinalQuestion()
    {
        bool res = false;
        if (MaxNum == MinNum)
        {
            res = true;
            _mesageBox.Show($"Вы загадали {MinNum} !");
        }
        return res;
    }
}

Вызов из кода программы:

private void btnSOLIDcode_Click(object sender, EventArgs e)
{
    var _gameSolid = new GameCycle(new GameQuestion(new MessageBoxAdapter()));
    _gameSolid.GameQuestion.MinNum = 1;
    _gameSolid.GameQuestion.MaxNum = 100;
    _gameSolid.Cycle();
}

Мы один метод, разбили на несколько простых классов, давайте посмотрим, какие у нас получатся тесты:

[TestFixture()]
public class GameQuestionTests
{
    [Test()]    ///интервал соседние числа, ответ на вопрос Yes
    public void RegularQuestionIntervalNeighboringNumbersYesTest()
    {
        //инициализация
        var moqMessageBoxList = new Moq.Mock<IMessageBoxAdapter>();
        moqMessageBoxList.Setup(a => a.Show("это число больше 23 ?", "Вопрос",
                                MessageBoxButtons.YesNo)).Returns(DialogResult.Yes);

        var mes = new GameQuestion(moqMessageBoxList.Object);
        mes.MinNum = 23;
        mes.MaxNum = 24;

        //тестируемый метод
        mes.RegularQuestion();

        Assert.AreEqual(24, mes.MinNum);
        Assert.AreEqual(24, mes.MaxNum);
    }

    [Test()]    ///интервал соседние числа, ответ на вопрос Yes
    public void RegularQuestionIntervalNeighboringNumbersNoTest()
    {
        //инициализация
        var moqMessageBoxList = new Moq.Mock<IMessageBoxAdapter>();
        moqMessageBoxList.Setup(a => a.Show("это число больше 23 ?", "Вопрос",
                                MessageBoxButtons.YesNo)).Returns(DialogResult.No);

        var mes = new GameQuestion(moqMessageBoxList.Object);
        mes.MinNum = 23;
        mes.MaxNum = 24;

        //тестируемый метод
        mes.RegularQuestion();

        Assert.AreEqual(23, mes.MinNum);
        Assert.AreEqual(23, mes.MaxNum);
    }

    [Test()]    ///Финальное сообщение, число угадано
    public void FinalQuestionMinEqMaxTest()
    {
        var moqMessageBoxList = new Moq.Mock<IMessageBoxAdapter>();

        var GameLogic = new GameQuestion(moqMessageBoxList.Object);
        GameLogic.MinNum = 23;
        GameLogic.MaxNum = 23;

        //тестируемый метод
        GameLogic.FinalQuestion();

        moqMessageBoxList.Verify(a => a.Show("Вы загадали 23 !"), Moq.Times.Once);
    }

    [Test()]    ///Финальное сообщение, число не угадано
    public void FinalQuestionMinNoEqMaxTest()
    {
        var moqMessageBoxList = new Moq.Mock<IMessageBoxAdapter>();

        var GameLogic = new GameQuestion(moqMessageBoxList.Object);
        GameLogic.MinNum = 23;
        GameLogic.MaxNum = 24;

        //тестируемый метод
        GameLogic.FinalQuestion();

        moqMessageBoxList.Verify(a => a.Show(Moq.It.IsAny<string>()), Moq.Times.Never);
    }
}

[Test(),
Timeout(5000)/*тестируемый метод может зациклится, предотвратим зависание лимитом на время исполнения*/]
public void CycleTest()
{
    //инициализация
    var gameLogic = new Moq.Mock<IGameQuestion>();
    gameLogic.Setup(a => a.RegularQuestion()).Returns(true);

    var gameCycle = new GameCycle(gameLogic.Object);

    //тестируемый метод
    gameCycle.Cycle();
    gameLogic.Verify(a => a.RegularQuestion(), Moq.Times.Once());
}

В чем разница между тестом метода с невыделенными ответственностями, и с тестами, где у каждой ответственности свой класс?

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

Спасибо за внимание и обратную связь в комментариях.
Мне очень важно знать что можно улучшить.
Поделиться публикацией
AdBlock похитил этот баннер, но баннеры не зубы — отрастут

Подробнее
Реклама

Комментарии 43

    +2

    К сожалению, фраза "писаться почти механически" оказалась применима и к самому коду: как ни странно, в нем содержится едва ли не больше ошибок проектирования, чем в исходном.


    Это одна из причин, почему принципы проектирования (очень) сложно показывать на простом коде.

      0

      Могли бы более подробно расказать про ошибки проектирования?
      А то могу спутать других, и для меня это на самом деле важно.
      Прежде чем протаскивать SOLID в промышленный код, нужно детально проработать на простых примерах.

        +1
        Могли бы более подробно расказать про ошибки проектирования?

        Я возьму сразу ваш финальный код.


        Вот он
        //ответственность: игровой цикл
        public class GameCycle
        {
            public IGameQuestion GameQuestion;
            private GameCycle() { }
            public GameCycle(IGameQuestion gameLogic)
            {
                GameQuestion = gameLogic;
            }
            public void Cycle()
            {
                while (!GameQuestion.RegularQuestion());
            }
        }
        //ответственность: Сообщения
        public interface IGameQuestion
        {
            int MaxNum { get; set; }
            int MinNum { get; set; }
        
            bool RegularQuestion();
            bool FinalQuestion();
        }
        //ответственность: Сообщения
        public class GameQuestion : IGameQuestion
        {
            IMessageBoxAdapter _mesageBox;
            private GameQuestion() { }
            public int MinNum { get; set; }
            public int MaxNum { get; set; }
        
            public GameQuestion(IMessageBoxAdapter mesageBox)
            {
                _mesageBox = mesageBox;
            }
        
            //обычные сообщения
            public bool RegularQuestion()
            {
                int medNum = (MinNum + MaxNum) / 2;
                var dr = _mesageBox.Show($"это число больше {medNum} ?", "Вопрос", MessageBoxButtons.YesNo);
                if (dr == DialogResult.Yes)
                    MinNum = medNum + 1;
                else
                    MaxNum = medNum;
        
                bool res = FinalQuestion();
        
                return res;
            }
        
            //финальное сообщение
            public bool FinalQuestion()
            {
                bool res = false;
                if (MaxNum == MinNum)
                {
                    res = true;
                    _mesageBox.Show($"Вы загадали {MinNum} !");
                }
                return res;
            }
        }
        public interface IMessageBoxAdapter
        {
            DialogResult Show(string mes);
            DialogResult Show(string text, string caption, MessageBoxButtons buttons);
        }
        public class MessageBoxAdapter : IMessageBoxAdapter
        {
            public DialogResult Show(string mes)
            {
                return MessageBox.Show(mes);
            }
            public DialogResult Show(string text, string caption, MessageBoxButtons buttons)
            {
                return MessageBox.Show(text, caption, buttons);
            }
        }
        
        private void btnSOLIDcode_Click(object sender, EventArgs e)
        {
            var _gameSolid = new GameCycle(new GameQuestion(new MessageBoxAdapter()));
            _gameSolid.GameQuestion.MinNum = 1;
            _gameSolid.GameQuestion.MaxNum = 100;
            _gameSolid.Cycle();
        }

        Ну и пойдем прямо сверху.


        Класс GameCycle настолько маленький, что не делает ничего полезного (что означает, что он лишний в этом коде), и при этом все равно содержит ошибки:


        1. GameQuestion не должен быть публичным, то, что эта функциональность куда-то вынесена — деталь реализации. Это хорошо видно в вызове: то, что вы делаете _gameSolid.GameQuestion.MinNum = 1; — нарушение Law of Demeter, а это признак того, что вы что-то делаете не так. И правда: вы здесь нарушили SRP, поскольку у этого кода появилась дополнительная причина для изменения — как только вы измените структуру GameCycle, вам придется изменить и этот код. До свидания, инкапсуляция.
        2. Даже если GameQuestion публичный, он не может изменяться снаружи, потому что это низачем не нужно, но может породить неожиданное поведение.
        3. Более того, если вдуматься, GameQuestion из тех же соображений не может изменяться никогда после создания GameCycle, а это значит, что он должен быть readonly.
        4. (это не дизайн, но все равно упомяну) вместо публичных полей рекомендуется использовать свойства, а private-конструктор без параметров при наличии любого другого не нужен.

        IGameQuestion:


        1. Название противоречит функции, потому что в реальности этот интерфейс отвечает не только за вопросы, но и вообще за все принятие решений.
        2. Из пункта выше становится понятно, что разделение ответственностей пошло не так (и почему настолько маленький класс GameCycle).
        3. Понять по названиям, что делает любой член этого интерфейса — невозможно.
        4. Метод FinalQuestion нигде не вызывается, что означает, что он лишний.
        5. Что будет, если в процессе использования этого интерфейса поменять значение в любом из свойств, и зачем это нужно?

        GameQuestion:


        1. Что будет, если в процессе использования этого интерфейса поменять значение в любом из свойств, и зачем это нужно?
        2. Метод FinalQuestion вызывается при каждом вызове RegularQuestion, а, значит, название не соответствует задаче. Вообще, разделение логики между этими двумя методами полностью надумано.

        Интерфейс IMessageBoxAdapter (и парный ему класс) совершенно избыточны; что хуже, интерфейс содержит две зависимости от System.Windows.Forms, хотя это низачем не нужно.


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


        public static int? GuessNumber(int minNum, int maxNum, Func<string, bool> asker)
        {
            do
            {
                int medNum = (minNum + maxNum) / 2;
                if (asker($"это число больше {medNum} ?"))
                    minNum = medNum + 1;
                else
                    maxNum = medNum;
        
                if (maxNum == minNum)
                    return minNum;
            }
            while (maxNum != minNum);
        
            return null;
        }
        
        private void LegacyCode_Click(object sender, EventArgs e)
        {
            var guess = GuessNumber(1, 100, s =>
                MessageBox.Show(s, "Вопрос", MessageBoxButtons.YesNo) == DialogResult.Yes
                );
            if (guess != null)
                MessageBox.Show($"Вы загадали {guess}!");
        }

        … и знаете, что самое занятное? Что SOLID соблюден никак не меньше, чем в вашем коде.


        Прежде чем протаскивать SOLID в промышленный код, нужно детально проработать на простых примерах.

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

          0

          Спасибо, а то варился в собственном соку, теперь подучил хороший фид бек.
          Жаль что не могу убрать в черновики статью.

            0
              0
              Зачем её убирать? Даже если вы считаете, что статья уже бесполезна, как минимум, в ней полезны комментарии.
              По теме: согласен с lair, особенно с использованием функции. На моём опыте, большинство функциональных решений как раз сочетают в себе ту самую простоту и элегантность, которой в ООП коде мне видится всё меньше (вполне вероятно из-за предвзятости в силу любви к ФП).
              Разумеется, есть и обратная сторона, когда объект с состоянием подойдёт куда лучше, чем функции с кучей замыканий.
        +3
        Самое смешное, что в итоге получилось просто гигантское кол-во кода, для такой элементарной вещи, боюсь представить что будет для реальных вещей. Если все упирается в тестируемость, то при TDD вы как ни крути будете писать код который работает так, как ожидается тестами и для этого не нужно писать тонны кода, он все равно никому не нужен будет через пару лет, все с нуля перепишется и будет работать ещё лучше, ещё быстрее и ещё надежнее, потом через пару опять этот цикл повторится и до тех пор пока проект не загнется
          0
          Ну как бы да, принципы SOLID — это совсем не про «немного кода для решения проблемы». Код становится куда объемнее, но и одновременно и более читаемым и легким к пониманию (в идеале)
            0
            Только вот практика показывает обратное
              0

              Практика — она разное показывает, но вот конкретно в посте точно не получилось "более легкий и читаемый".

                0

                Могли бы подсказать, как улучшить на этом учебном проекте?

                  0

                  Начать с того, что выбрать проект побольше.

                0
                Про это и была приписка «в идеале» :)
              0

              TDD не противоречит принципам SOLID, и так как результирующий код уже покрыт тестами, то его легче приводить в соответствие принципам.
              В первой итерации кода добавилось мало, там где просто добавили DIP, тем более если выкинуть тесты.
              Но сами тесты не элементарные.
              Разбив класс первой итерации, на более детальные ответственности, мы упростили тесты, но код стал более многословным.(но не более сложным). Платим за надежность и тестируемость, и расшираемость.
              Даже на коротком проекте (russian ai cup), под конец я столкнулся с проблемой, что мои изменения ломали текущий алгоритм.
              Поэтому обратил внимание на приципы.

                +1
                Я к тому, что код написанный не по SOLID, а чисто на основе опыта и здравого смысла, значительно легче читается/понимается, гораздо меньше строк кода и точно так же без проблем может быть покрыт тестами. Это справедливо конечно для подавляющего меньшинства программистов (я про умение писать такой код и не важно SOLID или не SOLID). Вот Keep It Simple и Don't Repeat Yourself это да, это реальная тема.
                А если можно писать замечательный код без SOLID, то зачем платить больше?
                  0
                  А если можно писать замечательный код без SOLID, то зачем платить больше?

                  Может, правда, внезапно оказаться, что этот "замечательный код" соответствует SOLID.

                    0
                    Не принципиально, если код расширяемый, масштабируемый, понятный, быстро работает и работает без ошибок, то он уже никому и ничем не обязан, и тем более ни чему не обязан соответствовать, он уже выполняет все, что требуется. А такие вещи как KISS и DRY это самое собой разумеющиеся на подсознательном уровне у грамотных специалистов. И специально не надо ему-то следовать, все это будет само собой разумеющееся. Если ты пытаешь чему-то специально следовать, то это характеризует тебя как не зрелого специалиста.
                      0
                      Не принципиально

                      "Не принципиально" для чего? Для понимания применимости SOLID — весьма принципиально.


                      А такие вещи как KISS и DRY это самое собой разумеющиеся на подсознательном уровне у грамотных специалистов.

                      … с рождения?


                      это характеризует тебя как не зрелого специалиста.

                      Все "зрелые специалисты" когда-то были незрелыми. Учиться тоже надо, и правила-принципы очень в этом помогают.

                        0
                        … с рождения?

                        Этим вещам не нужно специально учится, они на уровне под сознания сами по себе выполняются
                        Все «зрелые специалисты» когда-то были незрелыми. Учиться тоже надо, и правила-принципы очень в этом помогают.

                        Ну как вам сказать, если вы не думаете своей головой, а делаете так, как написано и так как вам говорит Вася. то специалистом вам не быть ни когда, так чисто средненькой рабочей силой для унылых задач
                          0
                          Этим вещам не нужно специально учится, они на уровне под сознания сами по себе выполняются

                          А с чего вы это взяли?


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


                          Ну как вам сказать, если вы не думаете своей головой

                          Следование принципам не исключает думания своей головой, даже наоборот.

                            0
                            А с чего вы это взяли?

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

                            Практика показывает обратное
                              0
                              С того, что это чистой воды здравый смысл

                              Что конкретно — "здравый смысл"?


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


                              Практика показывает обратное

                              Неа. Я знаю больше одного человека, которые следуют принципам и думают головой, следовательно, следование принципам думанья не исключает. Все достаточно просто.

                                0
                                Добавить что-либо/изменить что-либо и править/создавать по 10 файлов каждый раз, слабо тянет на здравый смысл
                                  0

                                  И как это (кроме слов "здравый смысл") связано с тем, что написано в моем комментарии?

                                    0
                                    Для изменения
                                    чего либо
                                    нужно изменить один клас который реализует функциональность этого
                                    чего либо
                                    .
                                    Это принцип единственной ответственности
                                      0

                                      … который в чистом своем виде не выполним почти никогда.

                                        0
                                        можно уйти в бесконечное дробление ответственностей?
                                          0

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

                      0
                      замечательный код
                      А как Вы можете охарактеризовать «замечательный код»? По каким критериям это можно определить?
                      легче читается/понимается
                      Как Вы предлагаете объективно оценить то, что это именно код легче понимается, а не так кажется субъективно просто его автору? Что вообще такое «легкость/сложность» кода, как ее измерить и от чего она зависит?
                        0
                        Когда смотришь на код сверху вниз, слева направо и понимаешь что происходит на каждой строчке, тогда это код является хорошим. А если чтобы понять каждую строчку надо лезть в несколько файлов каждый раз и ещё из них ветвления по разным файлам, ну это уже такое себе.
                          0
                          Когда смотришь на код сверху вниз, слева направо и понимаешь что происходит на каждой строчке, тогда это код является хорошим.

                          … что означает, что копипаста никак не мешает хорошему коду. А, следовательно, принцип DRY не так важен, как вы писали.

                            0
                            Скажем так, он менее важен чем KISS, но лично для меня DRY тоже важен, но опять же без фанатизма, везде есть предел.
                            +1
                            Когда смотришь на код сверху вниз, слева направо и понимаешь что происходит на каждой строчке, тогда это код является хорошим. А если чтобы понять каждую строчку надо лезть в несколько файлов каждый раз и ещё из них ветвления по разным файлам, ну это уже такое себе.
                            Но… разве не соответствует этому описанию листинг инструкций для регистров процессора?
                          0
                          SOLID сам по себе не является проблемой, каждый из принципов имеет смысл и вполне себе может здраво применяться. Другое дело, что типичное приложение не до конца ему следует, т.к. корректное применение этих принципов требует дисциплины, опыта и кругозора. Это в нашей индустрии наблюдается далеко не везде и я могу понять людей, которые ассоциируют SOLID с обязательной армией фабрик, адаптеров, провайдеров и т.п. Тем не менее, SOLID не про это и есть куда более оптимальные, и действительно лаконичные подходы, которые тоже следуют SOLID (я бы даже сказал, в большей степени).
                      0
                      Каким образом достигается сокращение расходов и повышение конкурентоспособности?

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

                      Хм. Вы серьезно считаете, что это объяснение? Обычно считается, что сокращение временных затрат связано с увеличением стоимости, а не сокращением расходов. Да и повышение качества обычно тоже не удешевляет продукт, а наоборот.
                        0
                        В других отраслях — да, но не в разработке ПО.
                          0
                          Не, погодите. Меня смущает вот что: тут упоминается в одной фразе «сокращение расходов и повышение конкурентоспособности». Если вы хотите сказать, что сокращение временных затрат на добавление функционала повышает конкурентоспособность — то тут я пожалуй согласен. Но как оно может одновременно сокращать расходы — я не улавливаю. По-моему так и в других отраслях ответ скорее нет.
                            0
                            Я хочу сказать, что это выражние:
                            Обычно считается, что сокращение временных затрат связано с увеличением стоимости, а не сокращением расходов. Да и повышение качества обычно тоже не удешевляет продукт, а наоборот.
                            ошибочно в контексте разработки ПО. Не знаю, прошли ли Вы по ссылке, но там написано: «In most contexts higher quality ⇒ expensive. But high internal quality of software allows us to develop features faster and cheaper.»

                            Разработка состоит из 4-х взаимосвязанных переменных: Cost, Time, Quality, Scope. Суть в том, что чем выше внутреннее качество ПО, тем быстрее и дешевле получается разработка (после достижения точки компромисса). Более того, — тем дешевле и быстрее изменение реализованных проектных решений. А это значит, что ПО может изменяться быстрее в ответ на скоротечно меняющиеся потребности рынка, предоставляя конкурентное превосходство своему владельцу.

                            В этой статье обсуждаются принципы SOLID, которые впервые были опубликованы в книге «Agile Software Development. Principles, Patterns, and Practices», которую Роберт Мартин выпустил на следующий год после того, как он организовал собрание 17-ти подписантов Agile Manifesto, среди которых присутствовал ряд известных архитекторов того времени. Улавливаете связь между архитектурой и Agile? И почему, после выпуска Agile Manifesto, первая книга Роберт Мартина была посвящена тому, как писать код, а не тому, как проводить стендапы? Какая связь между качеством кода и итеративным проектированием/разработкой?
                              0
                              >ошибочно в контексте разработки ПО
                              Ну так я сразу это и имел в виду. Не совсем в такой форме, но практически по тем же причинам.
                        0
                        В выходные буду работать над статьей, в верху есть очень полезные отзывы.
                        Просьба отнестись к статье конструктивно — критическии.
                          0
                          ИМХО, этот SOLID заставляет программиста становиться рабом лампы. Любое небольшое изменение кода перерастает изменение в 10 местах вместо одного. Запрет на редактирование ядра вызывает страшный код (тоже в нескольких местах) наследования…
                          Потому что, что думали вначале оказалось совсем не так, как нужно спустя год заказчикам.
                          Все с точностью наоборот. Больше непонятного кода, больше исправлений, больше правил. И предметная область исчезает за ворохом «правильного» кода. Видел, как ведущий разработчик над простым вопросом думал несколько дней. Решение он нашёл. И овцы целы, и волки сыты. Но оно того стоит?
                            0
                            ИМХО, этот SOLID заставляет программиста становиться рабом лампы. Любое небольшое изменение кода перерастает изменение в 10 местах вместо одного.

                            Это несколько противоречит идеям SOLID (не знаю, как насчет "этого", но оригинального).

                              0
                              Поддержу.
                              Любое небольшое изменение кода перерастает изменение в 10 местах вместо одного.
                              Это классифицированная проблема под названием Divergent Change и Shotgun Surgery. Изначальная идея SOLID направлена, как раз, на ее устранение.

                          Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                          Самое читаемое