Главные преимущества от соблюдения принципов SOLID – сокращение расходов на программный продукт и повышение конкурентоспособности программного продукта и команды.
Каким образом достигается сокращение расходов и повышение конкурентоспособности?
Давайте быстро пробежимся по принципам SOLID сточки зрения бизнеса:
Предполагается, вы знакомы с принципами SOLID, поэтому они не будут описываться. К тому же есть масса статей с объяснениями принципов SOLID.
Далее речь пойдет о применении принципов SOLID, для снижения убытков от ошибок в программном продукте, за счет более удобного тестирования.
Рассмотрим код, который угадывает задуманное человеком число:
Почему нельзя написать на функцию LegacyCode_Click юнит тест?
Этот код жестко зависит от статического класса MessageBox, который взаимодействует с внешней белковой системой (человеком), которая недоступна при запуске юнит тестов на сервере сборки. Другими словами, функция зависит от человека, которого нельзя включить в состав среды выполнения юнит тестов.
Как можно устранить зависимость выполнения кода от человека, или любой внешней системы, которую нежелательно встраивать в среду выполнения юнит тестирования?
Ответ: соблюсти принцип DIP (the Dependency Inversion Principle) принцип инверсии зависимости.
Что изменилось в методе? «MessageBox» заменили на «_mesageBox». Но если «MessageBox» это статический класс, который взаимодействует с пользователем, то «_mesageBox» это свойство класса объявленное через интерфейс в классе:
Объявление переменной «_mesageBox» с типом интерфейса и есть инверсия зависимости. При объявлении _mesageBox от конкретного класса, компилятор жестко связывает код с конкретным классом переменной, а объявление переменной от интерфейса дает свободу использовать любые экземпляры классов, которые реализуют интерфейс.
Для взаимодействия с пользователем нужно реализовать класс реализующий «IMessageBoxAdapter»:
Реализуем его через паттерн адаптер:
Вызов из формы будет, например таким:
Можно реализовывать различные «MessageBoxAdapter», которые смогут обращаться к разным классам, и метод «DoPlayGame» ни чего не будет об этом знать.
Давайте теперь рассмотрим, как можно тестировать метод «DoPlayGame». Какие есть сложности? Метод содержит цикл, а цикл может зациклится. К счастью в NUnit есть параметр «Timeout». Так как «DoPlayGame» внутри цикла содержит ветвления, это оператор if, и условие в while, то нужно как-то в тесте эмулировать нажатия на кнопки пользователя. При этом нажатия на кнопки должны быть продуманны на предмет того, чтобы все ветви кода были покрыты.
Для тестирования можно реализовать специализированный класс «MessageBoxList», который подставляет ответы пользователя из очереди:
Тогда тест будет таким:
Впрочем, специализированный класс для тестирования можно не писать, а пользоваться библиотекой Moq, в этом лучае, тест станет таким:
В чем принципиальная разница между тестом со специализированным классом «MessageBoxList», и тестом с использованием Moq?
Ответ: Метод со спец. Классом «MessageBoxList» дает больше гибкости, и он позволляет контролировать последовательность ответов тестируемого метода пользователю. Тест с использованием Moq, проверяет просто наличие ответов, но в какой последовательности они пришли, он не проверяет.
Как видим, для написания тестов пришлось немножко подумать, а тесты должны быть простыми, и писаться почти механически. Это вполне достижимо, если при написании кода соблюдать еще один принцип SOLID, который был нарушен, а именно единственной ответственности. Какие ответственности можно выделить в этом методе?
Выделим ответственности в другие классы:
Вызов из кода программы:
Мы один метод, разбили на несколько простых классов, давайте посмотрим, какие у нас получатся тесты:
В чем разница между тестом метода с невыделенными ответственностями, и с тестами, где у каждой ответственности свой класс?
Ответ: В том, что каждый тест во втором случае проверяет одну ответственность, и сделать это можно почти механически. Можно писать более сфокусированные тесты. В первом случае, когда в одном методе сосредоточились две ответственности, внимание расфокусировано на все две ответственности, в результате чего тесты сложнее писать, а самое главное они получаются менее качественными.
Спасибо за внимание и обратную связь в комментариях.
Мне очень важно знать что можно улучшить.
Каким образом достигается сокращение расходов и повышение конкурентоспособности?
- Сокращение временных затрат на добавление нового функционала. Если вышел на рынок первым, то захватишь его целиком.
- Сокращение убытков от ошибок в программном продукте, за счет повышения его качества.
Давайте быстро пробежимся по принципам SOLID сточки зрения бизнеса:
- Принцип единственной ответственности (The Single Responsibility Principle).
Если код соответствует этому принципу, то его легче понять, отладить, изменить, и оттестировать. Можно делать более масштабные изменения, так как если что-то сломается, то это что-то будет только одной функцией, а не целой системой. К тому же, одну функцию легче покрыть тестами, и проверить, что после изменений ни чего не сломалось. - Принцип открытости/закрытости (The Open Closed Principle).
При соответствии кода этому принципу, добавление нового функционала потребует минимального изменения существующего кода. А значит, это снижение времени на рефакторинг. - Принцип подстановки Барбары Лисков (The Liskov Substitution Principle).
При соблюдении этого принципа, можно классами наследниками, заменять классы родителей, без переписывания другого кода. Соблюдение этого принципа облегчает соблюдение принципа открытости закрытости. В результате, экономия времени и денег. - Принцип разделения интерфейса (The Interface Segregation Principle).
Этот принцип перекликается с принципом единственной ответственности. Разбивая интерфейсы по назначению, мы не заставляем реализовывать не нужные функции в классах реализующих интерфейсы, значит будет меньше кода, что скажется на скорости реализации, и экономии денег. - Принцип инверсии зависимостей (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());
}
В чем разница между тестом метода с невыделенными ответственностями, и с тестами, где у каждой ответственности свой класс?
Ответ: В том, что каждый тест во втором случае проверяет одну ответственность, и сделать это можно почти механически. Можно писать более сфокусированные тесты. В первом случае, когда в одном методе сосредоточились две ответственности, внимание расфокусировано на все две ответственности, в результате чего тесты сложнее писать, а самое главное они получаются менее качественными.
Спасибо за внимание и обратную связь в комментариях.
Мне очень важно знать что можно улучшить.
lair
К сожалению, фраза "писаться почти механически" оказалась применима и к самому коду: как ни странно, в нем содержится едва ли не больше ошибок проектирования, чем в исходном.
Это одна из причин, почему принципы проектирования (очень) сложно показывать на простом коде.
DiSur Автор
Могли бы более подробно расказать про ошибки проектирования?
А то могу спутать других, и для меня это на самом деле важно.
Прежде чем протаскивать SOLID в промышленный код, нужно детально проработать на простых примерах.
lair
Я возьму сразу ваш финальный код.
DiSur Автор
Спасибо, а то варился в собственном соку, теперь подучил хороший фид бек.
Жаль что не могу убрать в черновики статью.
lair
https://codereview.stackexchange.com/
Guzergus
Зачем её убирать? Даже если вы считаете, что статья уже бесполезна, как минимум, в ней полезны комментарии.
По теме: согласен с lair, особенно с использованием функции. На моём опыте, большинство функциональных решений как раз сочетают в себе ту самую простоту и элегантность, которой в ООП коде мне видится всё меньше (вполне вероятно из-за предвзятости в силу любви к ФП).
Разумеется, есть и обратная сторона, когда объект с состоянием подойдёт куда лучше, чем функции с кучей замыканий.
MaZaAa
Самое смешное, что в итоге получилось просто гигантское кол-во кода, для такой элементарной вещи, боюсь представить что будет для реальных вещей. Если все упирается в тестируемость, то при TDD вы как ни крути будете писать код который работает так, как ожидается тестами и для этого не нужно писать тонны кода, он все равно никому не нужен будет через пару лет, все с нуля перепишется и будет работать ещё лучше, ещё быстрее и ещё надежнее, потом через пару опять этот цикл повторится и до тех пор пока проект не загнется
Kenya
Ну как бы да, принципы SOLID — это совсем не про «немного кода для решения проблемы». Код становится куда объемнее, но и одновременно и более читаемым и легким к пониманию (в идеале)
MaZaAa
Только вот практика показывает обратное
lair
Практика — она разное показывает, но вот конкретно в посте точно не получилось "более легкий и читаемый".
DiSur Автор
Могли бы подсказать, как улучшить на этом учебном проекте?
lair
Начать с того, что выбрать проект побольше.
Kenya
Про это и была приписка «в идеале» :)
DiSur Автор
TDD не противоречит принципам SOLID, и так как результирующий код уже покрыт тестами, то его легче приводить в соответствие принципам.
В первой итерации кода добавилось мало, там где просто добавили DIP, тем более если выкинуть тесты.
Но сами тесты не элементарные.
Разбив класс первой итерации, на более детальные ответственности, мы упростили тесты, но код стал более многословным.(но не более сложным). Платим за надежность и тестируемость, и расшираемость.
Даже на коротком проекте (russian ai cup), под конец я столкнулся с проблемой, что мои изменения ломали текущий алгоритм.
Поэтому обратил внимание на приципы.
MaZaAa
Я к тому, что код написанный не по SOLID, а чисто на основе опыта и здравого смысла, значительно легче читается/понимается, гораздо меньше строк кода и точно так же без проблем может быть покрыт тестами. Это справедливо конечно для подавляющего меньшинства программистов (я про умение писать такой код и не важно SOLID или не SOLID). Вот Keep It Simple и Don't Repeat Yourself это да, это реальная тема.
А если можно писать замечательный код без SOLID, то зачем платить больше?
lair
Может, правда, внезапно оказаться, что этот "замечательный код" соответствует SOLID.
MaZaAa
Не принципиально, если код расширяемый, масштабируемый, понятный, быстро работает и работает без ошибок, то он уже никому и ничем не обязан, и тем более ни чему не обязан соответствовать, он уже выполняет все, что требуется. А такие вещи как KISS и DRY это самое собой разумеющиеся на подсознательном уровне у грамотных специалистов. И специально не надо ему-то следовать, все это будет само собой разумеющееся. Если ты пытаешь чему-то специально следовать, то это характеризует тебя как не зрелого специалиста.
lair
"Не принципиально" для чего? Для понимания применимости SOLID — весьма принципиально.
… с рождения?
Все "зрелые специалисты" когда-то были незрелыми. Учиться тоже надо, и правила-принципы очень в этом помогают.
MaZaAa
Этим вещам не нужно специально учится, они на уровне под сознания сами по себе выполняются
Ну как вам сказать, если вы не думаете своей головой, а делаете так, как написано и так как вам говорит Вася. то специалистом вам не быть ни когда, так чисто средненькой рабочей силой для унылых задач
lair
А с чего вы это взяли?
Впрочем, знаете, у меня для вас есть хороший, хотя и злой пример: "Правилам русского языка не надо специально учиться, они сами на уровне подсознания выполняются. Если ты пытаешься чему-то специально следовать, это характеризует тебя как неграмотного человека."
Следование принципам не исключает думания своей головой, даже наоборот.
MaZaAa
С того, что это чистой воды здравый смысл
Практика показывает обратное
lair
Что конкретно — "здравый смысл"?
Понимаете ли, со здравым смыслом есть несколько проблем. Во-первых, он тоже не дарован от рождения, он вырабатывается. Во-вторых, как следствие, он у разных людей разный (вот, например, у меня и у вас).
Неа. Я знаю больше одного человека, которые следуют принципам и думают головой, следовательно, следование принципам думанья не исключает. Все достаточно просто.
MaZaAa
Добавить что-либо/изменить что-либо и править/создавать по 10 файлов каждый раз, слабо тянет на здравый смысл
lair
И как это (кроме слов "здравый смысл") связано с тем, что написано в моем комментарии?
DiSur Автор
Для изменения
нужно изменить один клас который реализует функциональность этого .Это принцип единственной ответственности
lair
… который в чистом своем виде не выполним почти никогда.
DiSur Автор
можно уйти в бесконечное дробление ответственностей?
lair
Можно выяснить, что у любого кода больше всегда одной причины для изменения.
emacsway
Как Вы предлагаете объективно оценить то, что это именно код легче понимается, а не так кажется субъективно просто его автору? Что вообще такое «легкость/сложность» кода, как ее измерить и от чего она зависит?
MaZaAa
Когда смотришь на код сверху вниз, слева направо и понимаешь что происходит на каждой строчке, тогда это код является хорошим. А если чтобы понять каждую строчку надо лезть в несколько файлов каждый раз и ещё из них ветвления по разным файлам, ну это уже такое себе.
lair
… что означает, что копипаста никак не мешает хорошему коду. А, следовательно, принцип DRY не так важен, как вы писали.
MaZaAa
Скажем так, он менее важен чем KISS, но лично для меня DRY тоже важен, но опять же без фанатизма, везде есть предел.
emacsway
Guzergus
SOLID сам по себе не является проблемой, каждый из принципов имеет смысл и вполне себе может здраво применяться. Другое дело, что типичное приложение не до конца ему следует, т.к. корректное применение этих принципов требует дисциплины, опыта и кругозора. Это в нашей индустрии наблюдается далеко не везде и я могу понять людей, которые ассоциируют SOLID с обязательной армией фабрик, адаптеров, провайдеров и т.п. Тем не менее, SOLID не про это и есть куда более оптимальные, и действительно лаконичные подходы, которые тоже следуют SOLID (я бы даже сказал, в большей степени).
sshikov
Хм. Вы серьезно считаете, что это объяснение? Обычно считается, что сокращение временных затрат связано с увеличением стоимости, а не сокращением расходов. Да и повышение качества обычно тоже не удешевляет продукт, а наоборот.
emacsway
В других отраслях — да, но не в разработке ПО.
sshikov
Не, погодите. Меня смущает вот что: тут упоминается в одной фразе «сокращение расходов и повышение конкурентоспособности». Если вы хотите сказать, что сокращение временных затрат на добавление функционала повышает конкурентоспособность — то тут я пожалуй согласен. Но как оно может одновременно сокращать расходы — я не улавливаю. По-моему так и в других отраслях ответ скорее нет.
emacsway
Я хочу сказать, что это выражние:
ошибочно в контексте разработки ПО. Не знаю, прошли ли Вы по ссылке, но там написано: «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, первая книга Роберт Мартина была посвящена тому, как писать код, а не тому, как проводить стендапы? Какая связь между качеством кода и итеративным проектированием/разработкой?
sshikov
>ошибочно в контексте разработки ПО
Ну так я сразу это и имел в виду. Не совсем в такой форме, но практически по тем же причинам.
DiSur Автор
В выходные буду работать над статьей, в верху есть очень полезные отзывы.
Просьба отнестись к статье конструктивно — критическии.
olehrif
ИМХО, этот SOLID заставляет программиста становиться рабом лампы. Любое небольшое изменение кода перерастает изменение в 10 местах вместо одного. Запрет на редактирование ядра вызывает страшный код (тоже в нескольких местах) наследования…
Потому что, что думали вначале оказалось совсем не так, как нужно спустя год заказчикам.
Все с точностью наоборот. Больше непонятного кода, больше исправлений, больше правил. И предметная область исчезает за ворохом «правильного» кода. Видел, как ведущий разработчик над простым вопросом думал несколько дней. Решение он нашёл. И овцы целы, и волки сыты. Но оно того стоит?
lair
Это несколько противоречит идеям SOLID (не знаю, как насчет "этого", но оригинального).
emacsway
Поддержу.
Это классифицированная проблема под названием Divergent Change и Shotgun Surgery. Изначальная идея SOLID направлена, как раз, на ее устранение.