Недавно я получил письмо:
В течение многих лет проверка понимания принципов SOLID было стандартной частью нашей процедуры приема на работу. Предполагалось, что кандидаты хорошо знакомы с этими принципами. Однако в последнее время один из наших менеджеров, который больше не занимается программированием, усомнился в том, что это разумно. Его аргументы заключались в том, что принцип открытости-закрытости (open–closed principle) больше не очень важен, потому что большая часть кода, который мы пишем, не содержится в больших монолитах, а внесение изменений в небольшие микросервисы безопасно и легко. Принцип подстановки Лисков (Liskov substitution principle) давно устарел, потому что мы не уделяем столько внимания наследованию, как 20 лет назад. Я думаю, нам следует рассмотреть позицию Дэна Норта по SOLID: «Просто напишите простой код».
В ответ я написал следующее письмо:
Принципы SOLID остаются актуальными и сегодня, как и в 90-е годы (и даже до этого). Это потому, что программное обеспечение не сильно изменилось за все эти годы. Оно не сильно изменилось даже с 1945 года, когда Тьюринг написал первые строчки кода для электронного компьютера. Программное обеспечение по-прежнему представляет собой операторы if, циклы while и операторы присваивания - последовательность, выбор и итерацию.
Каждое новое поколение любит думать, что их мир сильно отличается от мира предыдущего поколения. Каждое новое поколение ошибается в этом. Это то, что каждое новое поколение узнает, когда приходит следующее новое поколение, чтобы рассказать им, насколько все изменилось. Итак, давайте рассмотрим принципы один за другим.
SRP - Принцип единой ответственности (The Single Responsibility Principle)
Соберите воедино то, что меняется по одним и тем же причинам. Разделяйте вещи, которые меняются по разным причинам.
Трудно представить, что этот принцип не применим в программном обеспечении. Мы не смешиваем бизнес-правила с кодом графического интерфейса. Мы не смешиваем SQL-запросы с протоколами связи. Мы храним код, который изменяется по разным причинам, отдельно, чтобы изменения одной части не нарушали работу других частей. Мы следим за тем, чтобы модули, которые изменяются по разным причинам, не имели запутывающих их зависимостей.
Микросервисы не решают эту проблему. Вы можете создать запутанный микросервис или запутанный набор микросервисов, если вы смешиваете код, который изменяется по разным причинам.
Слайды Дэна Норта полностью упускают суть этого вопроса и убеждают меня, что он вообще не понимал принципа. (или то, что он иронизировал, что, зная Дэна, гораздо более вероятно) Его ответ на SRP - «Напишите простой код». Я согласен. SRP - это один из способов упростить код.
OCP - Принцип открытости-закрытости (The Open-Closed Principle)
Модуль должен быть открыт для расширения, но закрыт для модификации.
Из всех принципов, мысль о том, что кто-то поставит под сомнение этот, наполняет меня страхом за будущее нашей отрасли. Конечно, мы хотим создавать модули, которые можно расширять, не изменяя их. Можете ли вы представить себе работу в системе, в которой отсутствует независимость от устройств, где запись в файл на диск принципиально отличается от записи на принтер, экран или канал? Хотим ли мы увидеть, рассредоточен ли оператор по нашему коду, чтобы иметь дело со всеми мелкими деталями?
Или… Хотим ли мы отделить абстрактные концепции от подробных понятий. Хотим ли мы изолировать бизнес-правила от мелких неприятных деталей графического интерфейса пользователя, протоколов связи микросервисов и произвольного поведения базы данных? Конечно!
Опять же, на слайде Дэна это совершенно неверно. При изменении требований неверна только часть существующего кода. Большая часть существующего кода все еще верна. И мы хотим быть уверены, что нам не нужно менять правильный код только для того, чтобы неправильный код снова заработал. Дэн ответил: «Напишите простой код». Опять же, я согласен. И, по иронии судьбы, он прав. Простой код бывает одновременно открытым и закрытым.
LSP - Принцип подстановки Лисков (The Liskov Substitution Principle)
Программа, использующая интерфейс, не должна быть сбита с толку реализацией этого интерфейса.
Люди (в том числе и я) совершили ошибку, сказав, что речь идет о наследовании. Нет. Речь идет о подтипе. Все реализации интерфейсов являются подтипами интерфейса. Все типы уток - это подтипы подразумеваемого интерфейса. И каждый пользователь базового интерфейса, заявленный или подразумеваемый, должен согласовать значение этого интерфейса. Если реализация вводит в заблуждение пользователя базового типа, тогда операторы if / switch будут распространяться по коду.
Этот принцип заключается в сохранении четкости абстракций и их точного определения. Невозможно поверить, что это устаревшее понятие.
Слайды Дэна полностью соответствуют этой теме. Он просто упустил суть принципа. Простой код - это код, который поддерживает четкие отношения подтипов.
ISP - Принцип разделения интерфейса (The Interface Segregation Principle)
Делайте интерфейсы небольшими, чтобы пользователи не зависели от ненужных вещей.
Мы по-прежнему работаем с компилируемыми языками. Мы по-прежнему зависим от дат модификации, чтобы определить, какие модули следует перекомпилировать и повторно развернуть. Пока это верно, нам придется столкнуться с проблемой, заключающейся в том, что, когда модуль A зависит от модуля B во время компиляции, но не во время выполнения, изменения в модуле B вызовут перекомпиляцию и повторное развертывание модуля A.
Эта проблема особенно остро стоит в языках со статической типизацией, таких как Java, C #, C ++, GO, Swift и т. Д. Языки с динамической типизацией затрагиваются гораздо меньше; но все же не застрахованы. Существование Maven и Leiningen является тому доказательством.
Слайд Дэна по этой теме явно некорректен. Клиенты действительно зависят от методов, которые они не вызывают, если их нужно перекомпилировать и повторно развернуть при изменении одного из этих методов. Если вы можете разделить класс с двумя интерфейсами на два отдельных класса, это хорошая идея (SRP). Но такое разделение часто невозможно и даже нежелательно.
DIP - Принцип инверсии зависимостей (The Dependency Inversion Principle)
Зависимость от абстракции. Модули высокого уровня не должны зависеть от деталей низкого уровня.
Трудно представить архитектуру, в которой этот принцип в значительной степени не использовался бы. Мы не хотим, чтобы наши бизнес-правила высокого уровня зависели от деталей низкого уровня. Надеюсь, это совершенно очевидно. Мы не хотим, чтобы вычисления, приносящие нам деньги, были загрязнены SQL, низкоуровневыми проверками или проблемами форматирования. Мы хотим изолировать абстракции высокого уровня от деталей низкого уровня. Это разделение достигается за счет тщательного управления зависимостями в системе, так что все зависимости исходного кода, особенно те, которые пересекают архитектурные границы, указывают на абстракции высокого уровня, а не на детали низкого уровня.
В каждом случае слайды Дэна заканчиваются словами: «Просто напишите простой код. Это хороший совет. Однако если годы и научили нас чему-то, так это тому, что простота требует дисциплины, основанной на принципах. Это те принципы, которые определяют простоту. Именно эти дисциплины вынуждают программистов создавать код, ориентированный на простоту.
Лучший способ устроить большой беспорядок - сказать всем: «Просто будьте простыми» и не давайте им никаких дальнейших указаний.
Комментарии (42)
Djeux
06.10.2021 15:55+13Снова микросервисами пытаются забить шурупы.
Если кто-то не способен написать хороший монолит с качественной архитектурой (а без хорошего понимания SOLID это сложно) то к микросервисам его точно подпускать не стоит.
artem_larin
06.10.2021 16:08+3>Лучший способ устроить большой беспорядок - сказать всем: «Просто будьте простыми» и не давайте им никаких дальнейших указаний.
Еще риторические вариации на эту тему, которые часто можно услышать: "Лучше руководствоваться здравым смыслом, чем SOLID, паттернами и проч.", "SOLID - это вкусовщина", "Давайте писать код, а не заниматься ерундой" и т.д. При этом частенько выясняется, что понимания этих принципов у утверждающего и нет вовсе, или оно неверное/сильно искаженное.
JSmitty
06.10.2021 16:49-5Как изящно он выворачивается из LSP :) Тогда ведь и аббревиатура рассыплется, некрасиво, сложнее кастомерам мозги полоскать. Парадигма изменилась, ООП во многих местах исчез или мутировал во что-то иное.
Реально - пишите простой код. Многотонные абстракции в каждой мелкой фигне - это просто дно. Просто положу это здесь - https://github.com/EnterpriseQualityCoding/FizzBuzzEnterpriseEdition
t13s
06.10.2021 16:59+5А что не так с LSP? Как по мне, в этой статье самое изящное изложение этого принципа из всех, что я слышал.
И во что у вас ООП мутировал, интересно?
JSmitty
06.10.2021 18:31-2Он же сам признается, что то, что под ним подразумевали раньше - и то, что считается сейчас - как бы несколько разные вещи?
ООП сейчас и ООП 20 лет назад - два разных ООП, не? Всякие ФП/ФРП, которые влетели во все мейнстрим языки - знатно далеки от изначальных концептов. Java и C++ - как два выживших "ООП" языка - очень язык не поворачивается назвать далее классическими носителями триады "инкапсуляция-наследование-полиморфизм".
PS enterprise fizz-buzz не смущает? Вот это всё про джунгли с гориллой к банану? Фабрики фабрик фабрик фабрик инструментов?
RH215
06.10.2021 18:56+3Он же сам признается, что то, что под ним подразумевали раньше - и то, что считается сейчас - как бы несколько разные вещи?
Тоже самое, акценты другие. В 90-х часто использовали наследование реализации, сейчас это считается скорее плохой практикой. А суть та же: код не должен видеть разницы между типом и его подтипами.
enterprise fizz-buzz не смущает?
Это оверинжениринг. Абстракции не обязательно должны быть сложными и многотонными, зависит от задачи.
nin-jin
07.10.2021 09:15+1Если фабрики/сиглтоны/адаптеры поменять на монады/моноиды/функторы получится такое же нагромождение абстракций для решение тривиальной задачи.
t13s
07.10.2021 09:43+1Он же сам признается
А мне ваше мнение интересно.
Всякие ФП/ФРП
вообще никак к ООП не относятся.
инкапсуляция-наследование-полиморфизм
строго говоря, к оригинальной идее ООП весьма перпендикулярны. Кроме того, не могли бы вы пояснить, как SOLID завязан на эту триаду?
enterprise fizz-buzz не смущает?
Смущает. Но это то же самое, что говорить, будто молоток - это плохой инструмент, только потому что кто-то им палец отшиб.
nin-jin
07.10.2021 09:06-3t13s
07.10.2021 09:23+2А в комментариях затем разбирается ошибка в рассуждениях автора.
nin-jin
07.10.2021 09:39-3Там человек ничего не понял просто, бывает.
t13s
07.10.2021 09:54Да пример просто немножко странный, поэтому, кмк, идею он раскрывает не очень: технически, вы пытаетесь сделать анбоксинг значения супертипа с явным приведением его к одному из подтипов - и на основании этого делаете вывод, что LSP не работает. Хотя за такой код надо просто линейкой по рукам бить:
void SomeFunction(Box<Animal> box) { Animal animal = box.Unbox(); Dog dog = (Dog)animal; // wtf? dog.Aport(); }
nin-jin
07.10.2021 12:02Я не делаю никакого анбоксинга. Функция просто засовывает кошку в клетку для животных. А ей передаётся клетка для собак.
t13s
07.10.2021 12:30То есть вы вот такой пример предполагаете?
class Box<T> where T:Animal { public void Put(T animal) { ... } } void PutAnimal(Box<Animal> box) { box.Put(new Cat()); } void Test() { PutAnimal(new Box<Dog>()); }
Так в этом случае оно даже не соберется в большинстве ЯП, потому что Box<Animal> не является супертипом для Box<Dog>. Чтобы оно заработало, вам надо явно указать вариантность.
А если "починить" проблему тем, что
class Box { public void Put(Animal animal) { ... } }
то рано или поздно столкнемся с проблемой явного приведения типов при использовании, о которой я написал выше.
nin-jin
07.10.2021 12:46-3Не утруждайте себя. В статье, на которую я там ссылаюсь, есть примеры кода в том числе и на сишарпе.
mayorovp
07.10.2021 14:49Это лишь доказывает, что "клетка для собак" не удовлетворяет контракту "клетки для животных".
Как вы из этого делаете вывод, что виноват LSP?
nin-jin
07.10.2021 14:58-2А удовлетворяет этому контракту любой супер тип, но не подтип.
mayorovp
07.10.2021 15:01Супертип чего?
В той "иерархии" типов, которую вы приводили в том посте, у "клетки для животных" на самом деле нет ни супертипов, ни подтипов.
nin-jin
07.10.2021 15:13+1Вы путаете subtyping и assignability.
Посмотрите примеры на языке D - это единственный язык, где компилятор действительно понимает вариантность. К сишарпу и яве её прикручивали сбоку изолентой.
mayorovp
07.10.2021 18:03Нет, в данном случае что-то путаете вы.
"Клетка для животных" и "клетка для кошек" — это разные объекты, которые не являются супертипом или субтипом друг для друга независимо от используемого языка программирования.
nin-jin
07.10.2021 19:08+1Тип - это множество возможных значений. Клетка для животных принимает все значения, что и клетка для кошек. Следовательно является её супертипом.
Ваше заблуждение по всей видимости исходит из того, что компиляторы некоторых языков не умеют в вариантность и тупо считают все дженерики по умолчанию инвариантными. Однако, обсуждаемый вопрос к дженерикам не имеет никакого отношения.
mayorovp
07.10.2021 19:23Клетка для животных принимает все значения, что и клетка для кошек.
С какого перепугу?
mayorovp
07.10.2021 20:58+2Но в клетку для животных можно положить собаку, а в клетке для кошек может сидеть только кошка. Эти два свойства являются взаимоисключающими, поэтому ни одна клетка не может быть клеткой для животных и клеткой для кошек одновременно.
Эти два множества не пересекаются, независимо от того сколько раз вы скажите одно и то же в разных формулировках.
ApeCoder
07.10.2021 23:29Человек немножко не разобрался, чем отличается сабтайпинг от дженериков.
У меня такое ощущение, что он прочитал, что надо подставлять типы а не объекты.
nin-jin
08.10.2021 07:03-1"клетка для животных" - это тип, а не значение. Значением является структура (кортеж) со ссылкой на животное. И эта структура является представителем как типа "клетка для животных" так и "клетка для собак".
Ну а дальше уже начинается специфика конкретных языков, которая вам всё никак не даёт покоя. Да, в разных языках есть те или иные ограничения.
Например, в номинативных приходится специально вручную разрешать подставлять подтип вместо супертипа. При этом компилятор должен разрешать это делать лишь для родственных типов. Но порой он и этого не делает.
У структурно типизированных языков же с пониманием типов всё традиционно гораздо лучше.
mayorovp
08.10.2021 10:20-1И эта структура является представителем как типа "клетка для животных" так и "клетка для собак".
Нет, не является.
ApeCoder
07.10.2021 23:37Тип - это множество возможных значений. Клетка для животных принимает все значения, что и клетка для кошек.
Тип клетки это множество возможных значений клеток. Клетка для собак это клетка для животных в том смысле, что собаки это животные, но клетка для собак это не клетка в которую можно положить любое животное. Например, слон в нее не уместится.
Клетка для животных в первом смысле не гарантирует того, что туда можно положить любое животное. Только то, что если оттуда удастся что-то достать, то это будет животное.
Клетка для любых животных и клетка для каких-то животных.
nin-jin
08.10.2021 07:44Клетка для животных именно что гарантирует, что туда можно поместить любое животное. А при доставании вы не получите ничего кроме животных. При этом гарантия "достать" распространяется на все подтипы. А гарантия "положить" на все супертипы. При этом речь идёт именно про depth subtyping. Width subtyping на эти ограничения никак не влияет.
ApeCoder
08.10.2021 09:25+2Очевидно тогда клетка для собак это не клетка для любого типа животных.
В нее нельзя положить слона,
А гарантия "положить" на все супертипы.
Это как - в клетку для животных можно положить что угодно?
Клетка_для_любого<T> инвариантна относительно T.
Как и требует LSP - мы не можем подменить клетку в которое гарантированно помещается любое животное на клетку которая гарантирует что там только собаки и принимает только собак как и наоборот.
Дженерики - это, фактически, функции в пространстве типов. В зависимости от свойств функций, порождаемые ими типы могут сабтайпиться в любом направлении по отношению к аргументам, но для корректной работы должны удовлетворять LSP.
nin-jin
08.10.2021 11:01Это как - в клетку для животных можно положить что угодно?
В клетку для животных можно положить и собаку.
Клетка_для_любого<T> инвариантна относительно T.
Клетка из которой и читают и пишут может быть как инвариантной, так и бивариантной в зависимости от кода функции. Это вообще не свойство самой клетки. Вариантность - это свойство кода, который с этой клеткой работает. И только он определяет какие типы взаимозаменяемы.
Дженерики
Обратите внимание, что в своих статьях я намеренно ничего не говорю про дженерики, ограничиваясь исключительно полиморфизмом подтипов.
ApeCoder
08.10.2021 12:36+1В клетку для животных можно положить и собаку.
Собака подтип, а не супертип
Вариантность - это свойство кода, который с этой клеткой работает.
А свойства должны быть таковы, чтобы соблюдать контракт клетки.
Клетка, которая принимает и выдает только собак не является клеткой которая принимает любое животное. Поэтому подстановка для них не работает.
не говорю про дженерики
Дженерик это функция которая делает из одного типа другой тип. То, что эта функция работает в голове а не выражена в ЯП на ход рассуждений не влияет.
nin-jin
08.10.2021 12:52-3Вы просто повторяете одно и то же. Я устал.
Дженерик - это сущность (функция, структура, класс и тд) принимающая обобщённые параметры. Таковыми параметрами могут быть не только типы, но и что угодно, даже кусок кода.
ApeCoder
08.10.2021 13:09+1Отлично, значит что-то принимающее тип и выдающее на его основе другой тип является дженериком. Напрмимер вы, когда берете тип животного и из него делаете тип клетки для этого типа животного. Но это не важно.
Клетка принимающая только собак, но при этом любую.собаку и гарантирующая, что выдает имеено собак не является ни под типом ни супертипом к клетке принимающей любое животное. На этом мы согласились?
Dekmabot
06.10.2021 16:51+1Знать что такие принципы есть и в чём они упрощают жизнь - важно.
А вот применять или нет - уже зависит от проекта и команды. В проектах с большим легаси и feature driven development - наверное далеко до solid.
SShtole
07.10.2021 10:13-4Программное обеспечение по-прежнему представляет собой операторы if, циклы while и операторы присваивания — последовательность, выбор и итерацию.
Да неужели. А я вот вижу, что всё больше задач решается «одним запросом на LINQ».Каждое новое поколение любит думать, что их мир сильно отличается от мира предыдущего поколения. Каждое новое поколение ошибается в этом.
То есть, прогресса в программировании нет. В принципе, после этого понятно всё.t13s
07.10.2021 11:07+1Как я понимаю автора, революции в программировании не случилось, есть эволюционное развитие.
И "один запрос на LINQ" внутри себя как раз и разворачивается в "if, циклы while и операторы присваивания".
В принципе, вам ничто не мешает взять какой-нибудь достаточно древний ЯП и забабахать LINQ на нем. В зависимости от древности и дубовости языка вам, конечно, придется пободаться с лямбдами и переизобретением expression trees - но на что-то принципиально нереализуемое вы вряд ли наткнетесь.
SShtole
07.10.2021 14:19-1Как я понимаю автора, революции в программировании не случилось, есть эволюционное развитие.
LINQ — это сильное изменение, за время жизни поколения глубоко укоренившееся в обиходе. Про него уже даже комиксы рисуют (с Джоном Скитом в главной роли). Но автор утверждает, что это заблуждение: таких изменений не бывает.И «один запрос на LINQ» внутри себя как раз и разворачивается в «if, циклы while и операторы присваивания».
Во-первых, можно точно так же сказать, что не существует ни if'а, ни while, ни операторов присваивания, потому, что на более нижнем этаже есть только два состояния и три оператора.
А, во-вторых, что более важно, речь идёт не про то, что там под капотом, а про промышленное программирование. И в нём налицо тенденция ухода от циклов к декларативщине при написании исходников в одних и тех же случаях. Чтобы такое не видеть, надо специально закрывать глаза.
NikolayPyanikov
07.10.2021 15:45+2Часто из программистов в менеджеры идут люди, у которых не сложилось с программированием. Не все конечно, но часто :) Почему, по тому же принципу, они не идут в стоматологи или не выбирают другую специальность? Очевидно, им будут необходимы соответствующие знания и опыт, но похоже у менеджеров такой проблемы нет :) Ничего не имею против менеджеров, но полностью доверять их мнению в разработке ПО не стоит. Выбор архитектуры монолитов, микросервисов и т.п. ни чего не говорит о том нужен ли там ООП и SOLID. И еще один момент - каждый раз, когда кто-то рассказывает как он понимает принципы SOLID они играют для меня новыми красками. Для себя я сделал несколько субъективных выводов:
- SOLID это не правила, а рекомендации о том, как делать программы в ООП
- они появились в результате многолетнего опыта использования ООП на практике и показывают путь «между граблями»
- они кончено как-то работают и по отдельности, но очень хорошо работают вместе, взаимно дополняя и усиливая друг друга
- основные цели: обуздать сложность, минимизировать ресурсы на разработку и поддержку
- очевидные преимущества: уменьшение связанности и дублирования, упрощение кода
oxidmod
07.10.2021 17:28+1Мне кажется, что примерно так и родился Laravel — автор просто решили писать простой код
Dmitriy_Volkov
08.10.2021 16:40Достаточно было сказать, что концепция микросервисов сама по себе базируется на SOLID
P.S. а SOLID в свою очередь это разжёванные киты ООП.
hlogeon
Как и с любым другим "принципом" актуальность зависит от контекста. Все эти принципы не имеют никакого смысла вне контекста