Pull to refresh

Comments 21

Принцип единственной ответственности (single responsibility principle): Модуль должен отвечать за одного и только за одного актора это подметил Р. Мартин в книге «Чистая архитектура. Искусство разработки программного обеспечения», когда описывал эволюцию данного определения.

Про единственную причину для изменений он же часто говорит в видео.

Довольно занятно, как в реальных проектах ломаются все озвученные принципы:

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

И это приводит к разрастанию очень и очень маленьких классов (буквально по одному методу), а потому становится очень сложно понять, что же происходит.

Если в алгоритмах нужно что-то поменять, то у нас нет причин изменять сам класс родитель. И в том и в другом случае это плюс к одной причине для изменения. +1 SRP

Не совсем. Если меняются сигнатуры, то это запросто повлечет изменения в куче мест. Более того, тут используется термин "класс родитель", а потому можно предположить, что предлагается делать наследование. Если так, то добавление нового аргумента в конструктор базового класса поменяет вообще все классы наследники.

Декоратор. Добавляем дополнительное поведение объекту, не меняя внутренности других декораторов и самого класса. +1 OCP

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

Liskov Substitution Principle - Принцип подстановки Барбары Лисков
Объекты в программе должны быть заменяемыми на экземпляры их подтипов без изменения правильности выполнения программы.

Если исключить самые-самые простые примеры, это принцип не работает даже в небольших проектах. Несмотря на то, что интерфейс к базе может быть общий, синтаксис запросов будет очень разным (как пример). Несмотря на то, что существует интерфейс List в Java (IList в .Net), в реальной программе всё-таки требуется знать, изменяемый объект или нет, это ArrayList или LinkedList и так далее. Аналогично про интерфейс Map (это может быть и хеш таблица, и дерево).

Interface Segregation Principle - Принцип разделения интерфейса
Клиенты не должны зависеть от методов, которые они не используют, в общем, это про правильное разделение интерфейсов.

Возможно, это странный перевод, так как википедия говорит, что:

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

И, в целом, идея даже правильная, вот только даже List, приведенный в пример выше, уже нарушает этот принцип. Собственно, если честно следовать этой логики, то у каждого интерфейса должен быть строго один метод, иначе можно быстро найти пример, когда код нарушает правило.

Модули верхних уровней не должны зависеть от модулей нижних уровней. Оба типа модулей должны зависеть от абстракций. То есть, абстракции не должны зависеть от деталей, детали должны зависеть от абстракций.

Это очень спорное правило, на самом деле. Как раз наоборот - если я использую драйвер к базе, я четко осознаю, как он работает. Если я читаю строки из файла, я именно понимаю, как всё будет происходить. Более того, если мы не делаем библиотеку, то мне намного легче видеть, что конкретно вызывается (с поправкой на unit тесты, где могут быть подмены), вместо того, чтобы иметь еще один проект посередине, чтобы каждый раз просить IDE перейти к реализации. Собственно, я привел примеры выше про List и Map.

Более того, если представить, что нам захочется сменить базу данных, то такая мелочь, как смена типов в программе, будет незаметна даже на фоне созвонов и договоренностей с Dev Ops.

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

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

Заодно правило выше будет толкать к написанию правильных текстов, к более читаемым и строгим типам, к наличию необходимой документации и корректного ревью кода.

Спасибо за ваш анализ, приятно было ознакомится. Решил так же прокомментировать некоторые части.

И это приводит к разрастанию очень и очень маленьких классов (буквально по одному методу), а потому становится очень сложно понять, что же происходит

....

И, в целом, идея даже правильная, вот только даже List, приведенный в пример выше, уже нарушает этот принцип. Собственно, если честно следовать этой логики, то у каждого интерфейса должен быть строго один метод, иначе можно быстро найти пример, когда код нарушает правило.

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

Если исключить самые-самые простые примеры, это принцип не работает даже в небольших проектах. Несмотря на то, что интерфейс к базе может быть общий, синтаксис запросов будет очень разным (как пример). Несмотря на то, что существует интерфейс List в Java (IList в .Net), в реальной программе всё-таки требуется знать, изменяемый объект или нет, это ArrayList или LinkedList и так далее. Аналогично про интерфейс Map (это может быть и хеш таблица, и дерево).

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

Не совсем. Если меняются сигнатуры, то это запросто повлечет изменения в куче мест. Более того, тут используется термин "класс родитель", а потому можно предположить, что предлагается делать наследование. Если так, то добавление нового аргумента в конструктор базового класса поменяет вообще все классы наследники.

Тут согласен, сигнатуры это, в некотором роде, Ахиллесова пята.

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

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

они ожидают одинаковой логики от этих абстракций, и в большинстве случаев получают ее.

Я не до конца этого понимаю.. Вот есть у меня Map в переменной. Могу ли я в цикле проверять "есть ли элемент в Map", если число проверок будет столько же, сколько и размер Map? Для HashMap - однозначно, а вот для TreeMap я свалюсь в NlogN (N проверок по logN), плюс в Java будут еще проблемы с нелокальностью памяти (но это, зачастую, уже мелочи).

Аналогично про List - отличный интерфейс, вот только могу ли я спокойно сохранить его в переменную (так как он неизменный, как и 99% List'ов в программе) или нет? А могу ли я добавлять туда элементы (так как это копия) или же я получу ошибку?

Ответы будут только тогда, когда я получу очень узкий интерфейс, который, по сути, будет полностью описывать реализацию.

они более всех ближе к той границе где за гранью хороших практик и все возможных общих советов уже идет сугубо индивидуальные задачи

Мне всё-таки так не кажется. Функциональный подход (для JVM это будет Scala/Kotlin) дает намного больше пользы. Он автоматически покрывает DDD (по сути, DDD из него и выходит), он сразу применяет Open-closed Principle (за счет того, что намного проще скрывать не только классы/методы, но и область видимости переменных) и так далее.

SPR не требует от нас ограничиваться одним методом, он лишь призывает к тому что бы класс, а именно его состояние и поведение, были ограничены одной зоной ответственности

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

Логика абстракций - это Map, карта на один ключ одно значение. Оно и используется для бизнес-операций. А вот в виде хэша или в виде дерева хранить данные внутри - это детали реализации. В этом и выгода идей Мартина. На верхнем уровне в бизнес-классе вы используете только переменную интерфейса. А какой конкретно класс реализует - бизнес класс знать не должен и ему неинтересно. Dependency Injection и решает эту задачу - при создании безнес-класса, в полях которого прописано Map map, внешний фабричный метод (или spring, для определенности), подсовывает нужный программисту объект класса реализации. Разработчик бизнес-класса не парится насчет списка. При необходимости настройки того, где конкретно будут храниться данные, можно будет легко сменить тип map, без исправления бизнес-класса.

Логирование это скорее сайд эффект поэтому бизнесу на него пофиг. Тем более вы только вызываете функции логгера. Если нужно будет поменять формат логгирования ты вы свой бизнес класс не должны менять.

Проверку входных данных лучше отделять от логики. Так тестировать проще.

И это приводит к разрастанию очень и очень маленьких классов (буквально по одному методу), а потому становится очень сложно понять, что же происходит.

Если у класса один метод, то этот класс скорее всего должен быть функцией.

Не надо путать единственность ответственности с единственностью метода.

UnmodifiableList в джава нарушает LSP. Это приводит к проблемам с использованием этого класса приведённого к типу List. Это как раз аргумент за то, что этот принцип работает.

ArrayList и LinkeList соблюдают этот принцип, поэтому вам не нужно знать кто именно из них передан.

Да, пару раз .stream(). ... .toList() заставляла меня побомбить.

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

Но отчасти не зная в точности SOLID, каждый программист бессознательно, но придерживается данных принципов, когда есть на то время))

...

Так же и в программировании, мало сказать что ты программист, здесь оооочень много вещей которые не делают нас программистами на деле

Я с вами отчасти согласен, и я свою статью начал и закончил похожими тейками.

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

То есть вынести какой-то алгоритм в отдельный класс, как вы написали - это ровно то, чего делать не нужно. Следует копипастить код, который нужен разным акторам согласно этому принципу.

Если что - я не сторонник SOLID, просто постарался максимально понять эти принципы.

Копипастить сразу не надо. А вот когда надо будет внести изменение нужное одному актору, нужно будет применить принцип закрытости и создать новый класс (копипаста, наследование, композиция).

А потом супер-пупер разработчики приходят с офигенным решением в бюджет, со знанием принципов SOLID, паттернов и всем остальным. Их просишь, банально два элемента местами поменять, один скрыть и начинаются миллионные контракты. Поэтому пофиг, как оно написано, главное, чтобы это все было расширяемым и легко поддерживаемым. Смотришь на этих супер прогеров и плакать хочется.

Когда уже Шаблонный метод начнут вносить в антипаттерны?

Он нарушает принцип composition over inheritance, и вынуждает реализации знать о внутренностях суперкласса

https://habr.com/ru/articles/325478/
В данной статье ознакомился с "Composition over inheritance", в общем смысле понимаю проблему, и к чему нас призывает сам термин, но даже в этой статье упоминается:

Наследуем, если:

  • Наследник является корректным подтипом (в терминах LSP — прим. пер.) предка

А вы как можете убедится, Шаблонный метод я упомянул вместе с LSP

Так же хочется отметить, что я описал проблемы которые встают перед разработчиками когда они активно используют наследование, те же не поддерживаемые методы из реализации `List.of` и тд.

Это все прекрасно в теории, но вот практика порой сама диктует подходы, и не всегда они слово в слово по учебнику. Существует множество способов сделать одно и то же, и некоторые из этих способов могут быть очень элегантны и понятны, хотя и не по учебнику, а другие - по книге, но абсолютно непрошибаемы. Только опыт решает. Лично для меня искусственное применение паттернов только для того, чтобы были - показатель плохого разработчика. Если же какой-то подход возникает после того, как человек увидел, как, используя те или иные концепции языка, можно написать удобно и просто - это высокий уровень.

Sign up to leave a comment.

Articles