Ещё будучи совсем-совсем начинающим разработчиком, я услышал про принципы SOLID и, конечно же, совершенно их не понял. Их не помогли понять ни лекции Дядюшки Боба, ни книга его же авторства, не несколько десятков (а то и сотен) статей в интернете, которые я тогда прочитал (по какой-то причине мне эти принципы казались чем-то очень важным, прямо-таки фундаментальным). Сейчас я могу сказать, что понимаю их как-то, по-своему, и пользуюсь ими каждый день. В данной короткой статье хочу поделиться своими размышлениями о сути принципов, которые были бы понятны тому мне из прошлого, который пытался загуглить доступное объяснение этих принципов.
Главный принцип разработки
Всю суть SOLID можно выразить простым допущением: "Как правило разработчикам приходится реализовывать новую функциональность, а не менять старую. Так давайте писать код так, чтобы новое было легко писать, а старое — тяжело сломать". Этот основной принцип совпадает с буквой О — open–closed principle, принципом открытости-закрытости. Все остальные принципы направлены именно на то, чтобы обеспечить именно такую структуру кода, при которой некоторый рефакторинг ничего не ломает, и в то же время новые фичи без проблем реализуются.
Буква S
Принцип единственной ответственности (он же single-responsibility principle) говорит о том, что какие-угодно блоки приложения (методы, классы, модули) лучше создавать такого размера, чтобы их не приходилось потом часто менять ("иметь одну и только одну причину для изменений"). То есть код написан один раз, и мы в принципе-то не хотим его менять. Но на всякий случай лучше сделать так, чтобы вероятность необходимости изменений была как можно более низкой.
Буква L
Принцип подстановки Барбары Лисков (Liskov Substitution Principle) говорит, что если мы дописываем новых наследников к классу, то нужно это делать таким образом, чтобы не пришлось менять весь старый код, который этих самых наследников будет использовать (старый код-то и не знает ничего про наследников). Опять-таки: легко дописываем, но тяжело ломаем.
Буква I
Принцип разделения интерфейса (interface segregation principle) — производный принцип от первой буквы, S. Дело в том, что сформулировать "единую ответственность" в терминах интерфейса бывает сложно, а этот принцип нам однозначно говорит: если какая-то реализация не использует некоторые методы интерфейса, то эти методы в интерфейсе лишние. Здесь проще всего показать на примере: в Java вот есть интерфейс Collection с методами в духе add(), remove() и так далее. И в неизменяемых реализациях коллекций эти методы, ясное дело, не нужны. Поэтому, согласно принципу I, интерфейс стоит разделить на Collection и его наследника, MutableCollection (как сделано в Kotlin, например). Цель у этого всего опять-таки одна: чем меньше методов в интерфейсе, тем реже его придется менять (и тем меньше шанс что-нибудь сломать). Ну и дописывать новые интерфейсы проще, чем дописывать методы в существующие интерфейсы.
Буква D
Принцип инверсии зависимостей (dependency inversion principle) — принцип-компаньон буквы L. Чтобы можно было легко дописывать наследников или менять реализации, нужно, чтобы код, который все это использует, не приходилось менять. То есть хочется, чтобы он зависел от чего-то постоянного, в определении принципа называемого "абстракцией". Принцип такой же, как и у остальных: мы ввели абстракцию, написали какой-то код опираясь на эту самую абстракцию. Затем можно писать разные реализации абстракции, менять реализацию уже написанных — на тот код, который опирается на неё, это не повлияет (при выполнении правила L, конечно).
Понятное дело, что мое понимание принципов не является единственно верным (а то и вообще неправильным). Однако, такое объяснение принципов мне видится очень понятным и прикладным.
Комментарии (35)
ExplodeMan
10.01.2022 17:11+1Для новичков статья в самый раз, пересказывает принципы солид простым языком.
Dekmabot
10.01.2022 17:16+18Статья лёгенькая, буквально в двух словах, поэтому позволю себе показать ещё более простой вариант на картинке под спойлером (автор неизвестен):
буквально в двух словах
apache2
10.01.2022 17:38+6Может это просто плохая идея если уже в 1000 раз повторяете а они все не могут вашу молитву выучить?
vt4a2h
10.01.2022 18:32+1Буква L
Принцип подстановки Барбары Лисков (Liskov Substitution Principle) говорит, что если мы дописываем новых наследников к классу, то нужно это делать таким образом, чтобы не пришлось менять весь старый код, который этих самых наследников будет использовать (старый код-то и не знает ничего про наследников). Опять-таки: легко дописываем, но тяжело ломаем.
Это один из наиболее важных принципов, который очень часто нарушают. Он больше про инвариант и котракты для входных аргументов и результата. Это в большей степени о том, чтобы код вёл себя более предсказуемо и был корректен, а не о лёгкости модификации или простоте сопровождения. Модифицировать как раз-таки может быть и не так просто, в особенности интерфейсы. А сломать-то уж проще простого.
nin-jin
10.01.2022 19:07+1funca
11.01.2022 00:30+1Ну ковартантность и LSP они обсуждали ещё в том же самом тредике, где Дядя Боб имел неосторожность предложить свои принципы (их там кстати больше) - https://groups.google.com/g/comp.object/c/WICPDcXAMG8?hl=en&pli=1#adee7e5bd99ab111.
Барбара в интервью рассказывала, что формулировала LSP как неформальное правило, больше с целью порассуждать, без претензий на научную строгость. У Дяди же адаптация принципа под OOD (с оглядкой на C++) выглядит вообще как каламбур. Это уже потом Дядю канонизировали, а сказанное превратили в догму.
nin-jin
11.01.2022 01:42+3Она там вводит определение подтипа, как то, что можно подставить вместо супертипа. А раз можно подставить массив животных в функцию, которая пушит в массив котят, и ничего не сломается, то массив животных, по её определению, получается подтипом массива котят.
Вообще, определение подтипизации, зависящее от того какие функции мы вызываем в программе, - это какое-то вырывание гланд через анус.
ApeCoder
11.01.2022 08:59+1Спасибо за ссылку
1987 - это ваша ссылка3.3. Type Hierarchy A type hierarchy is composed of subtypes and supertypes. The intuitive idea of a eubtype is one whose objects provide all the behavior of objects of another type (th e supertype) plus something extra. What is wanted here is something like the following substitution property [S]: If for each object o1 of type S there is an object o2 of type T such that for all programs P defined in terms of T, the behavior of P is unchanged when o1 is substituted for oz, then S is a subtype of T. (See also [2, 171 for other work in this area.)
Т.е. для всех программ, не для одной функции.
А вот википедия - 1994
Barbara Liskov and Jeannette Wing described the principle succinctly in a 1994 paper as follows:[1]Subtype Requirement: Let {\displaystyle \phi (x)}
be a property provable about objects {\displaystyle x} of type T. Then {\displaystyle \phi (y)} should be true for objects {\displaystyle y} of type S where S is a subtype of T.
Вообще, определение подтипизации, зависящее от того какие функции мы вызываем в программе, - это какое-то вырывание гланд через анус.
Этот документ целиком не читал, но как мне кажется по определнию выше, это ограничение скорее как нечто связывающее употребление и отношение между типами. Если оно не соблюдается, то что-то надо менять. Или программу или отношение.
Если программа делает то, что нужно, но принцип не соблюдается, возможно вам нужны другие отношения между типами.
funca
11.01.2022 12:51+1У нее есть более поздняя работа, где она придумала другое определение подтипов на полстраницы A behavioral notion of subtyping.
nin-jin
11.01.2022 13:31+1Для "всех возможных программ" по такому определению мутабельные типы вообще не образуют иерархии ибо являются инвариантными.
funca
11.01.2022 14:08Что такое инвариантные типы?
nin-jin
11.01.2022 15:11+1ApeCoder
11.01.2022 20:52+1Подставляются не типы а объекты типа. Конвариантны/контравариантны не типы а преобразования типов. Т.е. функция над типами. Ко-вариантны
Within the type system of a programming language, a typing rule or a type constructor is:
covariant if it preserves the ordering of types (≤), which orders types from more specific to more generic: If
A ≤ B
, thenI<A> ≤ I<B>
;contravariant if it reverses this ordering: If
A ≤ B
, thenI<B> ≤ I<A>
;bivariant if both of these apply (i.e., if
A ≤ B
, thenI<A> ≡ I<B>
);[1]variant if covariant, contravariant or bivariant;
invariant or nonvariant if not variant.
т.е. у вас есть спрособ описать функцию на пространстве типов f(x) -> y
если из x1 is x2 следует f(x1) is f(x2) то преобразование ковариантно. (Аргумент со-варьируется с результатом)Например в C# вот это не скомпилируется, так как List<> сам по себе не тип а фактически конструктор типов.
public static void Main() { object x = null; if (x is List<object> || x is List<>) { } }
Впрочем, мы это уже обсуждали
ApeCoder
11.01.2022 20:38Тут мне непонятно. Если есть коллекция ICollection с методами Add(Object x) и Object GetFirst() почему мутабельный List с такими же методами не является его подтипом?
funca
11.01.2022 10:24+2Это исследовательская работа, скорее уровня реферата. Она там собирает примеры абстракции данных в паре языков и в одном из разделов рассуждает про разницу между наследованием реализации и иерархией типов (а сейчас будет лучше сказать - спецификаций - потому, что ее трактовка типа там отличается от привычной из type theory). А сам "принцип" это цитата из другого автора, который она привела просто в качестве примера:
The intuitive idea of a subtype is one whose objects provide all the behavior of objects of another type (the supertype) plus something extra. What is wanted here is something like the following substitution property [6]: If for each object o1 of type S there is an object o2 of type T such that for all programs P defined in terms of T, the behavior of P is unchanged when o1 is substituted for o2, then S is a subtype of T. (See also [2, 17] for other work in this area.)
Я так понимаю LSP для ООП это стало с подачи дяди, сама Барбара ни чего такого не предполагала. Даже наоборот:
We are using the words “subtype” and “supertype” here to emphasize that now we are talking about a semantic distinction. By contrast, “subclass” and “superclass” are simply linguistic concepts in programming languages that allow programs to be built in a particular way. They can be used to implement subtypes, but also, as mentioned above, in other ways.
OlegZH
10.01.2022 22:19+1Почему-то в статье (да и в самой методологии) ничего не говорится о том, что нужны различные уровни описания. Дело в том, что, обычно, берётся иерерхия классов, инкапсулирующих важнейшие библиотеки, и создаётся новая иерархия классов. Точнее, создаются наследники существующих классов и соответствующие им иерархии. Классов (например, в Java) и так очень много, а тут ещё добавляется новая иерархия для описания предметной области. Тут зарыта довольно серьёзная собака. Системный подход не допускает смешение уровней описания, а это значит, что иерархия системных объектов и иерархия прикладных объектов — это две незавиисмые друг от друга иерархии. Вы берёте иерархию системных объектов и создаёте, по сути, новый язык описания, в терминах которого описываете уже объекты предметной области.
OlegZH
10.01.2022 22:30-2в Java вот есть интерфейс Collection с методами в духе add(), remove() и так далее. И в неизменяемых реализациях коллекций эти методы, ясное дело, не нужны. Поэтому, согласно принципу I, интерфейс стоит разделить на Collection и его наследника, MutableCollection (как сделано в Kotlin, например).
А не будет более естественным иметь единый класс Collection, но иметь параметр в конструкторе, который описывает поведение коллекции? (В некоторых ситуациях, можно было бы, даже, ожидать создания шаблона Collection.) По своей сути, коллекция — это абстрактный тип данных, который в самом общем виде описывает некоторый контейнер однородных (в определённом отношении) объектов. Но этот абстрактный тип инкапсулирует два следующих объекта: способ доступа и внутреннее представление.
Но, для того, чтобы я лучше понимал происходящее, было бы неплахо пояснить, что такое неизменяемые реализации коллекций.
csl
10.01.2022 23:11неизменяемые реализации коллекций
Подозреваю, топик стартер имел в виду, что в java.util.Collections есть методы .unmodifiableList, .unmodifiableSet, возвращающие экземпляры неизменяемых коллекций.
OlegZH
10.01.2022 22:33Совершенно выпустил из виду, что интересное начинается уж здесь:
в Java вот есть интерфейс Collection
То есть: колекция сама является интерфейсом (задумчиво уходит в совершенном замешательстве).
funca
10.01.2022 23:39Как правило разработчикам приходится реализовывать новую функциональность, а не менять старую
А вот в английской wiki про SOLID
design principles intended to make software designs more understandable, flexible, and maintainable
Т.е с точностью до наоборот - они ставят акцент на поддержке. Код живёт в разработке ну может пару дней (или до какой степени вы декомпозируете таски), поэтому данный этап не особо интереснен.
babayota_kun Автор
10.01.2022 23:53Так это ведь никак не противоречит статье: поддержка-то в основном заключается в допиливании новых фич или в исправлении багов.
Когда пилишь новую фичу будет хорошо, если это можно сделать просто и быстро, без глобальных изменений в коде. Когда фиксишь баг — хорошо, если изменения в одном месте не повлекут необходимости переписать пол проекта (и отломать кучу всего в придачу).Код живёт в разработке ну может пару дней
В этом же и суть: запилили фичу и забыли. Написанный код больше стараемся не трогать.
yurii_yakhnytsa
10.01.2022 23:49+2Статья простая и понятная. Много натыкался на статьи, которые можно объединить одной фразой "Не понимаю зачем в современном мире SOLID, никогда его не понимал, его написали старые бородатые дядьки под старое, никому не нужное ООП, юзать его не буду и вам не советую, на дворе 2020 + год", но человек не спасовал, разобрался, и написал свое понимание, и это хорошо, ибо новичкам будут попадаться выше упомянутые статейки, если не будет новой информации про опыт применения принципов SOLID.
А чисто от себя могу добавить, что понимание пришло довольно быстро. И на проекте на котором сейчас работаю благо все принципы закладывались со старта. И если какой либо принцип по той или иной причине нарушался было чувство, что ставишь на дороге грабли, и что вы думаете? Через пару месяцев эти грабли били по голове, и ты в очередной раз убеждаештся в их правомерности и актуальности.
agoncharov
11.01.2022 07:46+1Как правило разработчикам приходится реализовывать новую функциональность
Это неверное допущение. Принципы SOLID в первую очередь о том, как сделать так чтобы изменение существующей функциональности — равно как и добавление новой — не было сильно трудозатратым и не часто приводило к багам.
В частности принцип OCP предлагает проектировать код так, чтобы будущие изменения (как новаую функциональность так и модификацию старой) можно было осуществлять путём добавления новых программных сущностей, а не изменения существующих
devalio
12.01.2022 16:53Приз за самую короткую и бессодержательную статью о принципах SOLID сфоткаете?
"Как правило разработчикам приходится реализовывать новую функциональность, а не менять старую. Так давайте писать код так, чтобы новое было легко писать, а старое — тяжело сломать"
с этой фразой согласен
vgogolin
12.01.2022 20:57+1Учитывая интуитивное соотношение новых/существующих систем, как правило, приходится дополнять уже неотъемлемую старую функциональность. Слово "новое" в этом ключе не совсем верное. Больше похоже на приобревшее дальнейшее развитие старое. И поскольку мы, как правило, докладываем небольшие кирпичики к уже существующей большой куче, основной акцент должен быть на том, как эту кучу не разрушить.
pukinlou
а где статья?
babayota_kun Автор
С публикацией что-то пошло не так? Проверил через incognito — вроде бы доступна, корректно отображается.
MentalBlood
Наверное имелась ввиду низкая содержательность/объем
P.S.
А что в итоге помогло? Имхо, все эти принципы легко обнаруживаются с опытом как нечто само собой разумеющееся, далее уже оттачивание мастерства их применения
babayota_kun Автор
Да, опыт, конечно :)
Статья и правда очень куцая, но я в свое время не смог найти именно что-то подобного: очень простого изложения сути принципов в паре слов. Для более глубого изучения есть тысячи других статей и лекций, что и отражено в названии статьи
acmesun
Душнила