Главные преимущества от соблюдения принципов 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());
}

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

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

Спасибо за внимание и обратную связь в комментариях.
Мне очень важно знать что можно улучшить.

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


  1. lair
    13.11.2019 12:02
    +1

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


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


    1. DiSur Автор
      13.11.2019 19:32

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


      1. lair
        13.11.2019 20:11

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

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


        Вот он
        //ответственность: игровой цикл
        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();
        }


        1. DiSur Автор
          13.11.2019 20:28

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



          1. Guzergus
            14.11.2019 17:31

            Зачем её убирать? Даже если вы считаете, что статья уже бесполезна, как минимум, в ней полезны комментарии.
            По теме: согласен с lair, особенно с использованием функции. На моём опыте, большинство функциональных решений как раз сочетают в себе ту самую простоту и элегантность, которой в ООП коде мне видится всё меньше (вполне вероятно из-за предвзятости в силу любви к ФП).
            Разумеется, есть и обратная сторона, когда объект с состоянием подойдёт куда лучше, чем функции с кучей замыканий.


  1. MaZaAa
    13.11.2019 12:47

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


    1. Kenya
      13.11.2019 12:58

      Ну как бы да, принципы SOLID — это совсем не про «немного кода для решения проблемы». Код становится куда объемнее, но и одновременно и более читаемым и легким к пониманию (в идеале)


      1. MaZaAa
        13.11.2019 13:48

        Только вот практика показывает обратное


        1. lair
          13.11.2019 13:50

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


          1. DiSur Автор
            13.11.2019 19:48

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


            1. lair
              13.11.2019 20:11

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


        1. Kenya
          13.11.2019 16:21

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


    1. DiSur Автор
      13.11.2019 19:46

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


      1. MaZaAa
        13.11.2019 19:54
        +1

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


        1. lair
          13.11.2019 20:12

          А если можно писать замечательный код без SOLID, то зачем платить больше?

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


          1. MaZaAa
            13.11.2019 20:55

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


            1. lair
              14.11.2019 16:19

              Не принципиально

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


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

              … с рождения?


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

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


              1. MaZaAa
                14.11.2019 17:51

                … с рождения?

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

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


                1. lair
                  14.11.2019 17:53

                  Этим вещам не нужно специально учится, они на уровне под сознания сами по себе выполняются

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


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


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

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


                  1. MaZaAa
                    14.11.2019 20:44

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

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

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


                    1. lair
                      14.11.2019 21:00

                      С того, что это чистой воды здравый смысл

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


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


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

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


                      1. MaZaAa
                        14.11.2019 21:45

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


                        1. lair
                          14.11.2019 21:47

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


                        1. DiSur Автор
                          14.11.2019 22:55

                          Для изменения

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


                          1. lair
                            15.11.2019 00:06

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


                            1. DiSur Автор
                              15.11.2019 00:30

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


                              1. lair
                                15.11.2019 00:31

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


        1. emacsway
          14.11.2019 19:10

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


          1. MaZaAa
            15.11.2019 10:54

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


            1. lair
              15.11.2019 11:55

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

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


              1. MaZaAa
                15.11.2019 12:09

                Скажем так, он менее важен чем KISS, но лично для меня DRY тоже важен, но опять же без фанатизма, везде есть предел.


            1. emacsway
              15.11.2019 13:05
              +1

              Когда смотришь на код сверху вниз, слева направо и понимаешь что происходит на каждой строчке, тогда это код является хорошим. А если чтобы понять каждую строчку надо лезть в несколько файлов каждый раз и ещё из них ветвления по разным файлам, ну это уже такое себе.
              Но… разве не соответствует этому описанию листинг инструкций для регистров процессора?


        1. Guzergus
          14.11.2019 23:19

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


  1. sshikov
    13.11.2019 19:31

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

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

    Хм. Вы серьезно считаете, что это объяснение? Обычно считается, что сокращение временных затрат связано с увеличением стоимости, а не сокращением расходов. Да и повышение качества обычно тоже не удешевляет продукт, а наоборот.


    1. emacsway
      14.11.2019 18:57

      В других отраслях — да, но не в разработке ПО.


      1. sshikov
        14.11.2019 19:27

        Не, погодите. Меня смущает вот что: тут упоминается в одной фразе «сокращение расходов и повышение конкурентоспособности». Если вы хотите сказать, что сокращение временных затрат на добавление функционала повышает конкурентоспособность — то тут я пожалуй согласен. Но как оно может одновременно сокращать расходы — я не улавливаю. По-моему так и в других отраслях ответ скорее нет.


        1. emacsway
          14.11.2019 20:01

          Я хочу сказать, что это выражние:

          Обычно считается, что сокращение временных затрат связано с увеличением стоимости, а не сокращением расходов. Да и повышение качества обычно тоже не удешевляет продукт, а наоборот.
          ошибочно в контексте разработки ПО. Не знаю, прошли ли Вы по ссылке, но там написано: «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, первая книга Роберт Мартина была посвящена тому, как писать код, а не тому, как проводить стендапы? Какая связь между качеством кода и итеративным проектированием/разработкой?


          1. sshikov
            14.11.2019 21:22

            >ошибочно в контексте разработки ПО
            Ну так я сразу это и имел в виду. Не совсем в такой форме, но практически по тем же причинам.


  1. DiSur Автор
    14.11.2019 16:10

    В выходные буду работать над статьей, в верху есть очень полезные отзывы.
    Просьба отнестись к статье конструктивно — критическии.


  1. olehrif
    14.11.2019 18:13

    ИМХО, этот SOLID заставляет программиста становиться рабом лампы. Любое небольшое изменение кода перерастает изменение в 10 местах вместо одного. Запрет на редактирование ядра вызывает страшный код (тоже в нескольких местах) наследования…
    Потому что, что думали вначале оказалось совсем не так, как нужно спустя год заказчикам.
    Все с точностью наоборот. Больше непонятного кода, больше исправлений, больше правил. И предметная область исчезает за ворохом «правильного» кода. Видел, как ведущий разработчик над простым вопросом думал несколько дней. Решение он нашёл. И овцы целы, и волки сыты. Но оно того стоит?


    1. lair
      14.11.2019 18:35

      ИМХО, этот SOLID заставляет программиста становиться рабом лампы. Любое небольшое изменение кода перерастает изменение в 10 местах вместо одного.

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


      1. emacsway
        14.11.2019 18:42

        Поддержу.

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