Как стать автором
Обновить

Комментарии 26

Очередная попытка объяснить принципы SOLID на пальцах. Мне кажется что самый простой способ это сделать — это показать примеры кода «ка не надо делать» и «как надо делать».
И на каком же языке показывать? Имхо, абстракции самое то. Вы, как разработчик не командами же мыслите.
На псеводокоде можно. Мне бы подошел любой С-подобный, думаю большинство разработчиков также найдут его понятым. Я в целом о подходе «от негативного сценария». Если просто сказать что есть принципы и их нужно использовать — это такое себе. Если показать что бывает когда их не используешь — будет нагляднее, имхо.

А мне, как начинающему тестировщику, такой формат понятнее, чем куски кода. И я очень благодарна авторам, которые берутся вот так "на пальцах" и на картинках объяснять те вещи, которые вам - более опытным ребятам, могут казаться очевидными.

Open-Close не правильно изображен. Вместо процесса переделки должен создаваться второй робот, с обеими функциями, при этом первый оствется не тронутым и не останавливает свою работу.

И скорее даже первый робот торчит из второго вместо режущей руки.

Не обязательно.


SOLID — это не про ООП в условной Java — это шире.


Например есть консольная утилита sort — сортирует строки по не убыванию, и много утилит и людей использует её такой какая она есть.


Но тут становится понятно что иногда мы получаем не тот результат что нам нужен:


$ sort << EOF
> 1 one
> 2 two
> 10 ten
> 20 twenty
> EOF
10 ten
1 one
20 twenty
2 two

Нам хочется что бы числа интерпретировались именно как числа при сортировке (а не набор символов) что бы получать "математически правильную" сортировку.


У нас есть 2 пути — нарушить SOLID и изменить поведение утилиты — но тогда десятки/сотни уже написанных утилит (использующих sort) перестанут корректно работать.
Или не нарушать SOLID и расширить функциональность (добавив в данном случае опцию -g):


$ sort -g << EOF
1 one
2 two
10 ten
20 twenty
EOF
1 one
2 two
10 ten
20 twenty

И даже если мы говорим про классы — принцип Open-Close говорит нам, что если у нас есть условный класс Foo в версии 1.0, то в версии 1.1 у него могут появиться новые методы (или опциональные параметры к старым), но старые методы должны работать так как и раньше. И не обязательно для этого создавать новый класс потомок.


По большому счету Open-Close — это про обратную совместимость.

$ sort -g << EOF
По большому счету Open-Close — это про обратную совместимость.
Разве для того, чтобы добавить новую сортировку разработчикам не понадобилось модифицировать sort? В пределе, это приводит к созданию комбайнов (что нарушает SRP), появлению мертвого кода (когда в программе требуется только сортировка по числам, то не зачем тащить балластом сортировку по строкам) и т.п.

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

`sort -q` это пример разумного отступления от принципов SOLID в пользу упрощения.

Хорошим примером OCP является `git [command]`, где каждая команда реализуется отдельным файлом с именем `git-*`. Чтобы добавить новую команду не нужно менять сам `git` и ждать нового релиза. Достаточно создать новый файл с командой и поместить в PATH. Сам же `git` это просто единая точка входа для запуска таких команд и вывода подсказок.
Кмк, пример с «git [command]» больше напоминает DIP и SR, не в один в один, но все же, т.е. git — это верхний абстрактный уровень, который под собой использует специфичные реализации — отдельные файлы, каждый из которых хорошо делает свою работу (SR).
Разве нет?

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

У вас «правильный» пример Open-Closed нарушает Single Responsibility
Ага. Тут как-то так:
А умею резать полукольцами =>… => Я умею резать полукольцами и соломкой
По большому счету Open-Close — это про обратную совместимость

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

Это не значит, что если код ещё никто не видел, то класс нужно модифицировать, а не расширять. И это не значит, что если мы класс пишем с нуля, то можем в него напихать 50 методов, а вот если он уже написан — то методы добавлять нельзя.

И конечно, глупость считать, что ОпенКлоуз — про обратную совместимость. Она нужна далеко не всегда, а тут внезапно влезла в базовый принцип проектирования (которое часто вообще происходит до того, как у нас появляется хоть какое-то Legacy).
По-моему обратную совместимость обеспечивает LSP, а OCP просто не дает шаловливым ручкам залезать куда не просят.
Никакой из принципов ничего не говорит об обратной совместимости.
Более того, отсутствие обратной совместимости никак не противоречит принципам Solid.

Пример: у меня есть контракт, который следует принципам SOLID, в нём метод GetItem.

interface IContract {
  Item GetItem();
}


Теперь я понимаю, что есть более подходящее название для этого метода и произвожу рефакторинг:
interface IContract {
  Item GetOrCreateItem();
}


Код всё так же следует принципам SOLID (что OCP, что LSP), а обратная совместимость — сломана.
Следование принципам позволяет избежать типичных проблем. Если подставить вторую версию IContract туда, где ожидается первая, то программа сломается. Согласно LSP подобные типы не совместимы, и это говорит о том, что такой рефакторинг является небезопасным.
Это вы додумываете, ничего подобного в LSP не говорится.
SRP — по утверждению автора наиболее трудно понимаемый принцип.

Вы указали:

Каждый класс должен отвечать только за одну операцию.

В оригинале не совсем так. Такой принцип есть, но как утверждает сам автор «Uncle Bob», это не SRP.
Под SRP понимается следующее:

Модуль должен отвечать за одного и только за одного пользователя или заинтересованное лицо

Окончательная версия выглядит так:

Модуль должен отвечать за одного и только одного актора

В книге есть пример про клаcc Employee и 2 метода reportHours() и calculatePay(). Вроде все нормально, это все касается работника. Но на деле это может нарушать SRP по той причине, что разные отделы отвечают за эти методы.

reportHours() — Бухгалтерия
calculatePay() — Отдел по работе с персоналом.

И если делать изменения в соответствие с запросом одних, могут пострадать другие, если возьмем например тот факт, что оплата считается по часам.
НЛО прилетело и опубликовало эту надпись здесь

В данном контексте "актор" — это источник изменений, т.е. тот человек/отдел/роль от которого поступают требования на изменение данного класса. И идея данного принципа в том, чтобы избежать конфликта в требованиях — вероятность которого сильно увеличивается если требования поступают от разных источников, у которых отличаются намерения и приоритеты.

НЛО прилетело и опубликовало эту надпись здесь
Возможно у меня старческое бурчание, но объяснять эти принципы картинками похоже на движение ко дну.

Разве человек, который не способен понять SOLID из словесного описания и нескольких примеров сможет в дальнейшем манипулировать в своей голове объектами и понятиями достаточно хорошо, чтоб писать приличный код?
  1. Судя по тому, что SOLID регулярно постоянно обсуждают — всё это не так просто и однозначно, полученную информацию можно по разному понять, интепретировать и использовать. Причём обсуждают и примерами кода, и словесными описаниями, а тема никак не утихает.
  2. На эту того, что такое "приличный код" в холиварах тоже сломано немало копий.
  3. Навыки "понять из словесного описания" и "хорошо манипулировать в своей голове объектами и понятиями" (а это два отдельных, но связанных скилла) являются именно навыками — т.е. чем-то, что можно развить. Кому-то и такой формат в картинках может пойти на пользу, чтобы развить соотвествующий навык для дальнейшего использования.

Так что, имхо — нет, не движение на дно, а с точностью до наоборот.

Хорошая попытка, но увы не наглядно.
:-(

тот момент когда в картинках понял, а в коде не знаешь как реализовать. )))

Принцип единственной ответственности это когда класс имеет единственное назначение(а не операции). Операция,- это по своей сути действия обьекта. Например экскаваторщик может иметь два операционных набора, водить экскаватор и копать экскаватором.

Принцип открытости и закрытости означает что класс открыт для расширения, но закрыт для изменения. Дополнять класс методами само по себе не запрещено, но если расширять класс так как вы показали , вы автоматически нарушаете и принцип единственной ответственности и принцип разделения интерфейсов. Да и есть спорный момент- , при внесении в класс дополнительные публичные методы , о классе приходится знать больше чем оно необходимо. Например прогер знал о 2 методах в классе, то при расширении класса, прогеру придется обновлять эти знания. По своей сути класс должен единственный раз написан для продакшена, и в последующем закрыт для изменений и прямого расширения(по крайней мере его публичные части). Единственные причины для изменение класса -⅞ это работа над ошибками, рефакторинг и поддержка совместимости, не ломая API взаимодействия.

поэтому для расширения класса в ооп доступна способность наследования, а лучше применение наследования через паттерн композиции.

Принцип постановки Лисков

Это правило нынче уже более современно звучит и с новыми терминами.

По сути правило постановки лисков объединили с паттерном Контракты.

Да и если эту концепцию поддерживает язык ООП , то о нем сильно не стоит заморачиваться.

Помимо вами сказанного, реализуются дополнительные правила при переопределении класса дочерним классом:

  • Контравариантность типов параметров метода в подтипе. Или предусловия не могут быть усилены в подтипе. Т.е. типы параметров метода дочернего класса могут заменяться родительскими типами параметров метода основного класса.

  • Ковариация типов возвращаемых значений метода в подтипе. Или постусловия не могут быть ослаблены в подклассе. Т.е. тип результата метода дочернего класса может заменяться дочерним типом результата метода основного класса.

  • Инвариант не может быть ослаблен в подтипе. Т.е. тип свойства дочернего класса может иметь дочерний тип свойства осноного класса.

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

  • (исторические ограничения)неизменяемые данные базового класса не должны быть изменяемыми в подклассе. Если данные изменяемы, изменение таких данных правильней производить через методы основного класса.

    -(конец перечислений. Далее редактор с параграфами хрень исполняет.)

    По сути правило постановки лисков объединили с паттерном Контракты.

Зарегистрируйтесь на Хабре, чтобы оставить комментарий