Я профессионально занимаюсь веб разработкой более 25 лет, за это время у меня накопился достаточно большой опыт, но некоторые вещи всегда вызывали противоречие, среди них:
SOLID - аббревиатура обозначающая 5 принципов, которые по утверждению автора должны помочь содержать кодовую базу чистой и простой. Но что может быть не так?
SOLID всегда продавался подавался именно в таком звучании и в таком порядке, хотя сами принципы совершенно независимые друг от друга. Слово как бы само за себя говорит что придерживаться принципов СОЛИДно. Отсюда можно сделать вывод что смыслы, вложенные в аббревиатуру были с большой долей вероятности натянуты, чтобы образовать в итоге красивое слово. Возможно есть какие-то другие принципы, более полезные, но не вошедшие в понятие.
Marketing first
Но нельзя делать выводы поверхностно. Давайте копать глубже. Программирование оно бывает сильно разное. Между компилируемым и интерпретируемым языками существует большая разница и принципы в какой-то степени должны учитывать это.
В дополнении - в интернете нету единственного правильного описания принципов. Эти принципы настолько сложны, что каждый пытается понять их выдавая своё мнение за единственно правильное. А это значит только одно - в любой момент вы можете встретить человека, который не правильно эти принципы понимает и следовательно - если он будет их придерживаться - поломает код еще сильнее, независимо от того какой он сейчас)
И в целом сами принципы в определенных обстоятельствах лучше чем их отсутствие, но когда это возводится в абсолют - код начинает дурно пахнуть
Давайте перейдем к деталям:
S - SRP - Single responsability - Принцип единственной ответственности
У сущности (класса) должна быть только одна причина для изменений
Каждая сущность (класс) должен иметь только одну зону ответственности
Здравый смысл подсказывает, что в этих утверждениях есть здравый смысл. Действительно, хорошо когда система спроектирована так, что за какое-то конкретное действие отвечает конкретный класс и не нужно прыгать по всему коду, пытаясь отыскать все схожие методы. И Это хорошо подходит для сервисных классов: Подключение к базе - отдельный класс, подключение к почте - отдельный класс, подключение к смс - отдельный класс
Перебор начинается, когда пытаются на каждый метод в классе создать отдельный класс - типо сотрудник - он может отчеты генерить, а может ходить. И этот пример конечно абстрактный, но исходя только из этих утверждений - совершенно не ясно как следует поступить. Типо это два разных действия и разная зона ответственности.
Ещё один ошибочный вывод который делают исходя из метода - это то что класс должен быть как можно меньше. Это дробление совершенно точно не приведет к упрощению кода. Это напоминает мне попытку упростить код переписав монолит на микросервисы (возможно однажды тоже об этом поговорим). Сложность кодовой базы если ее разделить на множество классов не станет меньше, но работать с ней станет сложнее, если на каждый чих будет создан отдельный класс. Посмотрете например на класс Eloqment в Laravel. Он ведь тоже по СОЛИДу написан. Класс содержит 2433 строчки. Ничего себе
O - OCP - Open-closed principe - Принцип открытости-закрытости
Сущность должна быть открыта для расширения и закрыта для изменения
Так. Чтобы разобраться в этой каше - нужно понимать что такое открыт, закрыт, расширение и изменение в контексте класса. Все ли понимают эти термины одинаково? Расширение - это вероятно наследование, а изменение - это видимо замена кода. Кажется что автор хотел сказать что единожды написанный класс никогда не должен быть изменен, а всегда должен меняться через наследование. А что может значить фраза открыт? Типо можно, а закрыт - типо нельзя?
Боже. Если если это действительно всё так - почему автор так не назвал метод. Расширяй вместо изменений. Зачем эти слова открыт-закрыт. Они вносят путаницу. Предполагаю что ответом на этот вопрос будет то о чем я писал в самом начале - Маркетинг. Из буквы Р хорошее слово не построишь
А может быть автор имел в виду совсем другое? Кажется что нет, но зачем эта путаница нужна в лингвистике
Возможно этот тезис писался во времена, когда систем контроля версии не существовало, либо для компилируемых языков - где лишние уровни абстракции влияют только на время компиляции, а не на время выполнения кода. А также во времена когда не было тестов
Вот скажите, в проекте, которому допустим год - сколько раз вы вносили изменения в классы? Могу по своему опыту сказать, что это происходит постоянно, особенно в стартапах, где заранее ничего наверняка неизвестно. И аргумент, что прошлая версия кода всем нравилась (была протестирована), а новая - неизвестно, по этому старую версию трогать не нужно, а новую нужно наследовать от старой - это бред.
Во первых - можно ведь не наследовать, а заменить класс целиком. Можно ведь?
Во вторых - Есть система контроля версии на случай если вдруг старая версия кода представляла какую-то ценность
В третьих - Если есть сомнения в новой версии кода - пишутся тесты покрывающие все сомнительные критичные места и тесты пишутся вместе с правками. Таким образом - новая версия будет ничуть не хуже по качеству старой
Но если вы всегда будете наследовать - это точно путь в никуда. Никто так не делает, даже сами адепты СОЛИДа
Кажется что принцип можно перефразировать как "Работает - не трогай" и применим он только для заросших плесенью проектов, приводя к ещё большему запутыванию кода
Но не мог же автор прямо такие вредные советы давать. Возможно я не правильно его понял. Под каким углом ещё можно понять автора?
Может быть он имел в виду, что Вот есть какой-то написанный кусок кода и новый функционал, который как бы дополняет нужно писать вообще в отдельных классах (Привет SRP). Возможно, но если это так - то хватило бы и SRP.
Или может быть он имел в виду, что квадрат - это такой же равносторонний треугольник, но число сторон = 4? То есть зачем мне дублировать код, когда меняется лишь один параметр. Тут у меня тоже есть вопросы: Кажется что фигуры находятся на одном уровне иерархии. Наследование будет определяться от того какая фигура первой была написана (исторически сложилось)? И что делать, когда нужно будет изобразить круг?
Даже если представить иерархию в моделях - Есть класс персонаж и есть класс рыцарь, а также например класс лучник. У них разница может быть описана в дистанции атаки, а также например в скорости. Логично?
Допустим, но где профит? Какой из классов мне не нужно изменять? Персонаж? Но если нужно будет дописать новое свойство - его же все равно нужно будет изменить? А что будет, если завтра появится новый класс лекарь, который например не умеет атаковать?
Единственное - в последнем примере - код в общий класс вносится реже, а также есть наследование. Кажется что этот пример лучше всего подходит под описание. И кажется что он имеет больше всего здравого смысла, чем всё остальное. Но у меня все равно много вопросов. Неужели нельзя было понятнее составить описание метода, использовать больше понятных фраз. Если все всё сразу поймут - то кто будет консультации заказывать
Кажется что описание настолько широкое, а единственное полезное применение настолько узкое, что имеет смысл обычное неудачное название
L - LSP - Liskov Substitution Principle - Принцип подстановки Барбары Лисков
Принцип можно описать, как Класс наследник нужно проектировать таким образом, чтобы его можно было свободно подставить вместо базового класса
Другими словами класс наследник в наследуемых функциях - типы входов и выходов должны совпадать, а также диапазон значений входных параметров должен быть не шире чем у базового класса, а диапазон выходных значений - не уже чем у базового класса
В противном случае - при подмене может возникнуть ошибка взаимодействия. В этом определении я уверен и тут вопросов практически нету. Всё логично. Но все ли понимают этот принцип именно так?
И почему тут все приводят в качестве примера квадрат и треугольник?..
I - Interface Segregation Principle - Принцип разделения интерфейсов
Много подходящих интерфейсов лучше чем один общий
Здесь кажется всё логично. Класс может реализовать несколько интерфейсов, а значит - разделить их не будет ошибкой по реализуемым функциям
Другое дело что в интерпретируемых языках - интерфейсы либо сильно упрощены, либо приносят дополнительную обработку в итоге, не приводя ценности
D - Dependency Inversion Principle - Принцип инверсии зависимостей
Модули верхних уровней не должны зависеть от модулей нижних уровней. Оба типа должны зависеть от абстракций. Абстракции не должны зависеть от деталей. Детали должны зависеть от абстракций
Очень много вводных. Что такое модуль (верхний, нижний)? Что такое абстракция? Что такое деталь? Как выглядит эта зависимость? Тут явно замешан какой-то контекст. Как обычно - каждый понимает как может
Верхний уровень - это вероятно всего бизнес-логика. Нижний уровень - скорее всего что-то приближенное к базе данных, или допустим к рендеру.
Зависимость вероятнее всего можно описать как передачу экземпляра класса в качестве параметра в методе другого класса. Тут конечно много разных вариаций что внутрь чего можно передать и зачем
Следовательно передавать допустим экземпляр подключения к базе в метод получения данных пользователя напрямую - будет нарушением принципа.
Но как поступить?
Кажется что конструкция недостаточно гибка. Ведь может поменяться обращение к базе, либо сам юзер. Но зачем оптимизировать то, чего нет преждевременно?
Да и если что вдруг - можно ведь подменить поведение через подстановку класса-заменителя.
Вводить абстракцию между модулем верхнего и нижнего уровня - звучит словно вводится две зависимости вместо одной. Но ради чего?
В качестве аналогии в голову приходит проектирование скелета с чрезмерно большим количеством суставов. Гибкость увеличивается, но возрастает сложность
+ Тут ещё не раскрыта тема деталей.
Холиварная тема
Поправьте меня, если я не прав
Видите, как легко ошибиться в этой каше
В моей картине мира - чистый и простой код - это тот которого нету, а функционал при этом работает как было задумано. Поскольку каждый кусок кода всегда содержит в себе такое бремя, как объем. Чем больше кода - тем сложнее его читать, каким бы простым и понятным он ни был. Это объем, который нужно хранить, компилировать, сопровождать, копировать. В этом плане KISS и DRY гораздо ближе к чистоте и пониманию кажется
Только что, собрав все размышления и догадки по поводу единства смыслов SOLID - я прихожу к выводу, что SOLID - это прежде всего - про безопасность вас в команде как разработчика.
Вот разработчик A написал класс. Вам нужно внести изменения в него. Если вы напрямую будете менять класс - получится так что вы внесли изменения в чужой код - а значит теперь вы отвечаете за баги и ошибки в нем полностью. Теперь понятно про какую именно ответственность говорится в S. Про то что нельзя изменять существующее - в O, про то чтобы можно было подставить ваш класс вместо исходного так чтобы он не поломал код в L.