Comments 163
Если вы знакомы с Clean Architecture и не боитесь Clojure, то стоит просмотреть мой проект
- исходники: https://github.com/darkleaf/publicator
- описание: https://github.com/darkleaf/building-application
Вам не кажется, что вы не разобравшись в первоисточниках вводите людей взаблуждение?
Придумал, а точнее взял Coupling/Cohesion, вывел из них красивые буквы SOLID и сделал их модными именно Роберт Мартин. Тут ошибки нет.
В статье же сказано, что он придумал сами принципы, а это не соответсвует действительности.
Вот как раз красивые буквы SOLID вывел Michael Feathers
SRP: The Single Responsibility PrincipleТо есть если метод состоит из 3k+ строк он соответствует этому принципу главное что бы изменения запрашивались от одной роли?
В этом принципе речь идет о том, что изменения в модуле может запрашивать одна и только одна роль. Например, есть модуль, реализующий некую бизнес-логику, запросить изменения в этом модуле может только Аналитик, но никак не DBA или UX.
OCP: The Open Closed PrincipleЗачем же тогда отдельно выделены dependency inversion principle?
…
Используя dependency inversion, наш модуль объявляет только интерфейс отправки уведомлений, но не реализацию.
То есть если метод состоит из 3k+ строк он соответствует этому принципу главное что бы изменения запрашивались от одной роли?
Да, такое возможно. Но маловероятно — обычно методы такого размера притягивают к себе дополнительные ответственности в процессе эволюции кода.
Да, такое возможно. Но маловероятно — обычно методы такого размера притягивают к себе дополнительные ответственности в процессе эволюции кода.Это же вы написали
Если автор не использует какого-то термина — то мне его тоже нельзя использовать? Хорошо, переформулирую.
Обычно методы такого размера притягивают к себе изменения от разных ролей.
Если вы пишете обычно, значит описанная мной ситуация возможна.
Так все же такой метод соответствует SRP?
Зачем же тогда отдельно выделены dependency inversion principle?
Потому что не получилась бы красивая аббревиатура :) В SOLID часть принципов прилично накладываются на остальные.
Тут речь о том, что у вас есть файл, разделяемая библиотека, микросервис и т.п. и должна быть возможность расширить его поведение без редактирования, перекомпиляции. А как именно это будет сделано — не особо важно. Можно через dependency injection, monkey patching и т.п.
Например, есть модуль, реализующий некую бизнес-логику, запросить изменения в этом модуле может только Аналитик, но никак не DBA или UX.
Тогда, если двум объектам понадобится изменить один объект, то они попросят сделать это Аналитика.
А разве это не нарушит принцип, так как аналитика будут дергать сразу двое?
И вроде уже давно разобрались что именно эта формулировка как раз и не верная…
Объект не может о чем-то попросить Аналитика, потому что Аналитик — роль в команде, а не объект и не модуль :-)
То есть два других актора могут попросить одного Аналитика что-то сделать?
конечно, программа подстраивается под отношения людей, а не наоборот
Могут. Но к разработчику он должен обратиться не в форме "меня тут попросили...", а в форме "сделай мне...". А разработчик ему может ответить "не буду, это не твой уровень абстракции" :)
Не совсем. Если говорить о скраме, и вообще о роялях в создании и эксплуатации инженерных систем, то в данной интерпретации у каждой сущности системы должен быть только один стейкхолдер.
Это создает такой избыточный геморой на всем этапе анализа, что не в сказке сказать. А все ради того чтобы программист чуть-чуть по-другому структурировал код. Сдается мне — это фантастика.
Вряд ли 4-й клерк 128-го отделения банка вообще знает о существовании product owner. Непосредственному начальству сообщит или в саппорт. И так или иначе пожелание дойдёт до стейкхолдера (например, директора операционного департамента), который либо передаст его product owner, либо получит от него это пожелание для утверждения/отклонения.
Тут речь не о том, что все пожелания директора опердепа в одном модуле, а о "списке" модулей, изменения в которые вносятся только с его подачи с одной стороны, а, с другой, отклоняются, если они относятся к другому модулю. Ну или принимаются другие меры, оставляющие за каждым стейкхолдером его "личные" модули, например, перераспределение обязанностей в бизнес-процессах или переразбиения на модули.
Согласно такой логики комманда должна написать некий объект, который будет заниматься биллингом, нотификациями и управленем базой данных.
Ну, скорее или кучу объектов таких в каждом из модулей, которая (куча) будут принадлежать ему (директору), или отсылать его (хорошо звучит...) к стейкхолдерам модулей в которые он хочет влезть.
Объявит интерфейс, со 100500 зависимостями и единственным методом Run(). Сделает одну уникальную его реализацию в которую запихнет вызов репозитория, который в свою очередь тоже будет реализовывать Command pattern. И после 10 вызовов зависимостей, которые только и делают что вызывают зависимости — внизу будет хранимая процедура, которая посчитает биллинг, проставит флаги и отправит email. И вот эта процедура и будет соответствовать SOLID.
Сдается мне, что в статье на задана достаточность условия.
Хоть Product owner и управляет бэклогом, но сами задачи в бэклоге исходят не от него.
Вы бы сначала почитали
SOLID говорит строить зависимости на абстракциях.
Ваши репозитории зависят на AbstractPersistor или PersistorInterface.
В начале реализовали PostgreSQLPersistor который имплементирует абстракцию. Захотелось использовать MariaDB имплементировали MariaPersistor и с помощью dependency injection подменили.
SOLID вам только помогает в этом вопросе
А по поводу 3-го пункта — так надо иногда делать, ибо выбранная в начале пути технология может не обеспечить удовлетворения новых требований, предъявляемых проекту. Если этого не сделать — вы просто навредите проекту.
Технологии не приколачиваются гвоздями к решению.
История следующая. Обычно все говорят, зачем тебе абстракции, если ты и так знаешь что будешь использовать условный Postgres. У тебя вопрос — полная противоположность.
Для начала работы как раз нужно использовать заглушку, но понимать, что примерно будет использоваться потом. И спроектировать адекватные абстракции.
Если ты ожидаешь, что будет использоваться реляционная база, а потом окажется, что можно использовать только key-value, то абстракции нужно будет менять и переписывать проект.
Откладывание слишком большого числа технических решений ведёт к страшному усложнению взаимодействия простых вроде бы компонентов (переслоёности), увеличению времени на разработку прототипа, повышению порога входа в проект, невозможности использования уникальных для конкретной реализации фич и другим гадостям. К тому же, абстракции постоянно текут.
Можно провести такую аналогию. Заказчик говорит: Нужно отправить письмо, если (тут набор бизнес требований). Т.е. он не говорит, я хочу отправить письмо через Mailchimp. Он закрывается абстракцией и откладывает решение о реализации.
Про проводу протекания абстракций. Текут абстракции, вроде TCP, которые обязуются обеспечить надежность поверх ненадежных технологий. Если выдернуть провод, TCP естественно не доставит пакет.
Все зависит от архитектора и его компетенций. Само-собой можно такого напроектировать.
Просмотри первоисточники, там подробно разобраны твои вопросы.
В случае письма у нас:
- Требования ясны с самого начала.
- Абстракция довольно проста (стоимость внедрения стремится к нулю).
- Мотивация смены последующего способа отправки, как и его вероятность высока и понятна.
Очевидно, что тут не сделать небольшую абстракцию — преступление.
В случае с той же СУБД всё уже не так просто.
Про случай СУБД, советую посмотреть https://github.com/darkleaf/building-application
Чтобы вставить заглушку — нужно объявить интерфейс(абстракцию).
Давай пример попроще.
Допустим, нужно отправлять уведомления.
Мы объявляем интерфейс вроде IMailSender
с одним методом void Send(address, title, content)
.
Далее описываем бизнес-логку. В тесах используем mock/stub/fake object с интерфейсом IMailSender.
Нам важно примерно знать что будет, а не точно.
Найди книжку Clean Architecture, там все доступно объясняется.
(простите за некропостинг)
Конечно выесняется, мы еще не научились предсказывать будущее.
Ты ожидал чего-то другого? =)
Если отправка письма — это абстракция, и нужно изменить способ с smtp на mailchimp, то меняется только реализация. Даже не нужно перекомпилировать модуль с бизнес логикой.
Если изначально не нужна была скрытая копия, а теперь нужна — то это изменени бизнес логики. И требуется ее изменение и перекомпиляция модуля.
посмотри еще вот это: https://github.com/darkleaf/building-application
там пример на clojure, но все доступно объясняется
Другое дело, что не все абстракции одинаково полезны.
Про ту же СУБД. Мы можем ввести абстракцию TableGateway или Repository. Обе они прячут имплементацию БД. Но первая, скорее всего, треснет, как только надо будет поменять тип хранилища с реляционого на ключ-значение. Но никаких проблем в смене MySQL на другую SQL базу не возникнет. А Repository будет жить даже в любом случае, но нужен ли он в приложении — другой вопрос.
Ну а по поводу потратить время\деньги — нет ничего хуже, чем проигнорировать процесс сбора требований. Если не изучать вопрос хоть как-нибудь, вы рискуете потратить гораздо больше, чем потратили бы на ресерч.
Я его понял по другому: мы уже знаем что будет за абстракцией 100% ближайшие 10 лет, но всё равно лучше ввести абстракцию.
Введение абстракции никак не влияет будет работать первая версия или нет. Ну, считая, что абстракция применена правильно.
Смотря что абстрагировать. Взять те же СУБД. Подход с репозиториями верный, но не самый быстрый. Ну, допустим, сделаем.
В реализации репозиториев завязываться на конкретную СУБД по полной и фигачить внутри SQL? А если заменить надо будет через 10 лет… хммм… начинаем смотреть на PostgreSQL и MySQL. Понимаем что мало того, что придётся прижаться и юзать SQL99 и выкинуть половину крутых фич конкретной базы сразу (и это верно для большинства готовых data mapper-ов и AR), так ещё и не совсем тривиальные обёртки написать для типов данных, JSON и так далее.
Итого время на реализацию такой обёртки составляет треть от времени реализации начальной версии проекта… а может всё-таки зафиксировать что у нас будет всегда PostgreSQL и не страдать фигнёй?
Мы можем создать абстракцию, максимально приближенную к целевой реализации, например, вынося в интерфейс возможности PostgreSQL, которых в обозримом будущем в MySQL не появится. Но всё-таки абстрагируясь от деталей реализации этой возможности PostgreSQL, например Repository.getNextId => this.query('SELECT next_val('seq_name'))
- Целевая реализация 10 лет не будет стоять на месте.
- Если отталкиваться только от PostgreSQL, переезд, например, на MySQL обернётся такими костылями, что лучше и не пытаться.
- Альтернативная тоже не будет, например в MySQL появилась поддержка JSON. С другой стороны, какая-то используемая функциональность может быть объявлена deprecated и потом вообще выпилена, с предложением более эффективного альтернативного способа получения того же результата. А у нас получение результата за абстракцией, в одном месте (в идеале) точечено меняем реализацию и всё.
- А на Oracle можно и попытаться.
По опыту, введение репозиториев практически пренебрежимо увеличивает затраты на начальную разработку в двух случаях:
- используемая инфраструктура хорошо заточена под них
- не предпринимаются попытки написать универсальные реализации, просто обычный SQL-код или обращения к NoSQL хранилищам, а также какие-то ORM/ODM прячутся за фасадом репозитория, не оперирующего терминами выбранной системы хранения или её классом, а оперирующего терминами абстрактного хранилища.
Естественно, в предположении что члены команды достаточно хорошо понимают что это такое и как его реализовывать, пускай даже большинство не понимают зачем.
Да, так будет работать и в этом есть смысл, но это далеко не "легко заменить одну базу на другую", как любят приводить в пример.
Относительно легко. Как минимум не нужно будет рыскать по всему коду приложения в поисках SQL :)
А примеры на то и примеры, чтобы лишь демонстрировать идею на понятных всем вещах.
Нужно будет рыскать по всем репозиториям, да. Это лучше, чем вообще везде.
Как по мне, пример с заменой СУБД — это один из самых плохих примеров потому как в нём не ясно и спорно всё от и до. Вот пример с отсылкой почты или SMS прост, понятен, мало затратен и имеет смысл в подавляющем большинстве случаев.
И тогда надо будет рыскать по всем репозиториям, всем процедурам и всему коду, который все это вызывает.
Согласен. СУБД слишком сложные, чтобы просто получить легкую замену одной на другой, используя их максимально полно. А частота этого примера провоцирует множественное создание велосипедов, типа это же легко, сейчас замутим.
> достаточно хорошо понимают что это такое и как его
> реализовывать, пускай даже большинство не понимают
> зачем.
Если у нас есть идеально работающая комманда, в которой все разработчики хорошо обучены и сработаны и плюс под управлением тех. лида, который знает что делает — то как мне сдается даже без формализованых SOLID принципов все будет хорошо. Даже без следования оным их код будет функциональным и поддерживаемым.
В других случаях репозитории — не гиря на ногах, конечно. Но как минимум наручник пристегнутый на икру.
Ну, принципы SOLID как бы и направлены на улучшение поддерживаемости. К ним, как и к паттернам нередко приходишь сам, а потом узнаешь, что всю жизнь разговаривал прозой :) Главная заслуга их создателей, что они дали и популяризировали краткие и запоминающиеся имена решениям, к которым многие приходят методом проб и ошибок.
Я бы тупой репозиторий с кучей findBy… методов сравнил с рукавицей, а с поддержкой полноценных критериев — с перчаткой.
Я немного не то имел в виду. В хорошей команде скорее всего SOLID принципы в большой мере соблюдаются даже без формальных знаний о них. В "плохой" их надо внедрять "из под палки", чтобы обеспечить приемлемое качество кода.
"Рукавица" зачастую лучше. У "перчатки" внезапно может оказаться слишком много вариантов её конфигурации и логика конфигурирования полезет вон из репозитория.
- А не достаточно ли нам в этом случае репозиториев? Да, поправить, возможно, придётся не в одном месте, а в 10-20, но это не так затратно, как разработка хорошего слоя абстракции реляционной базы (в рамках одного проекта это не выглядит реалистичным в принципе).
- Тоже есть отличия, но да. Тут уже попытка не будет столь болезненной, если не очень сильно отходить от SQL99.
- Не очень понял. Имеете в виду создание конкретных репозиториев без реализации ими абстрактного интерфейса репозитория? Если так, то, с одной стороны, это незначительно экономит время по сравнению с реализацией, с другой — лишь незначительно экономит, а защищает от протечек абстракции лучше.
Имею ввиду не абстрагировать внутренности метода репозитория никак. То есть использовать там SQL или пользоваться простой готовой обёрткой. Не пытаться сделать аналог DQL или HQL, стараясь абстрагировать специфичные фичи PostgreSQL на низком уровне.
Это, конечно, да. Для подавляющего большинства проектов это будет очень неоправданным расходованием ресурсов. В целом я о более высоуровневых абстракциях. На низком уровне или использовать что-то готовое, или писать низкоуровневый код, спрятанный за достаточно высоким уровнем абстракции. Или комбинировать, например, для простого CRUD брать ORM в качестве основной реализации репозитория, а для тяжелых вещей типа отчётов писать непосредственную работу с базой.
> реализации репозитория
Вот почему-то большинство писателей репозиториев, результат работы которых доводилось видеть, не рассматривает это как вариант.
но только в последний год осознал, что они означают
Нет, судя по тексту статьи — все еще не осознали.
На каждом проекте люди играют разные роли (actor): Аналитик, Проектировщик интерфейсов, Администратор баз данных. Естественно, один человек может играть сразу несколько ролей. В этом принципе речь идет о том, что изменения в модуле может запрашивать одна и только одна роль. Например, есть модуль, реализующий некую бизнес-логику, запросить изменения в этом модуле может только Аналитик, но никак не DBA или UX.
А если на проекте появляются новые роли — надо все переписывать?
Это определенно может ввести в ступор. Как можно расширить поведение класса без его модификации?
Наследование, вариации паттерна «Стратегия» и так далее. При чем тут dll и jar?
LSP: The Liskov Substitution Principle
Есть класс Квадрат и есть класс Прямоугольник. Что от чего наследовать?
Под интерфейсом здесь понимается именно Java, C# интерфейс. Разделение интерфейса облегчает использование и тестирование модулей.
Так как же их разделять-то?
Что такое зависимость модулей? Это ссылка на модуль в исходном коде, т.е. import, require и т.п. С помощью динамического полиморфизма в runtime можно обратить эту зависимость.
Нет, зависимость — это зависимость между двумя любыми структурными блоками программы (функциями, классами, etc).
В общем, в книжке дяди Боба все есть, очень подробно и с примерами кода. А это стоит убрать.
Есть класс Квадрат и есть класс Прямоугольник. Что от чего наследовать?
Какое отношение этот вопрос имеет к статье?
Ну хорошо, попробую ответить на него. В иммутабельном виде Квадрат должен быть наследником Прямоугольника, потому что квадрат — это просто прямоугольник специального вида. В изменяемом виде ни один из этих классов не может быть наследником другого — это нарушит LSP.
Например, ее приводит сам Роберт Мартин — web.archive.org/web/20151128004108/http://www.objectmentor.com/resources/articles/lsp.pdf
А в википедии ей посвященая отдельная страница — en.wikipedia.org/wiki/Circle-ellipse_problem
Я хотел намекнуть, что осознание автором принципа LSP далеко не полное, и, что не стоит пользоваться его формулировками.
Задача про квадрат и прямоугольник — это классическая задача на которой этот вопрос немного проясняют.
А в оригинальной статье Лисков тем временем (http://reports-archive.adm.cs.cmu.edu/anon/1999/CMU-CS-99-156.ps) даны очень четкие определения того, что считать инвариантами и ограничениями и как правильно наследовать.
А о том, какие условия должны выполняться, чтобы оно соблюдалось?
1. Есть иммутабельный тип.
2. Есть функция его использующая.
3. Наследуем от иммутабельного типа новый — мутабельный (добавляем сеттеры, например).
По определению Мартина — это корректный код, функция может использовать мутабельный тип вместо базового иммутабельного.
По определению Лисков — нет, потому что нарушены инварианты.
По определению Мартина — это корректный код, функция может использовать мутабельный тип вместо базового иммутабельного.
Не любая функция, а только та, которая не полагается на инвариант. Та, которая полагается, использовать уже не сможет, потому что поломается.
Определение Мартина использует конструкцию "функции… должны иметь возможность" — т.е. речь идет сразу о всех потенциальных функциях принимающих базовый класс.
Таким образом, согласно определению Мартина ваш пример нарушает LSP. Не вижу разницы с определением Лисков.
CLASSES MUST BE ABLE TO USE OBJECTS OF DERIVED CLASSES
WITHOUT KNOWING IT.»
Нету здесь речи ни о всех потенциальных функциях ни о функциях полагающихся на инвариант. Это вы уже дополняете, потому что знаете о чем правило. И прочитано это может быть как угодно (о чем и речь, собственно).
FUNCTIONS… MUST BE ABLE
Все точно так же как и в русском переводе. Функции должны иметь возможность.
Это вы уже дополняете, потому что знаете о чем правило.
Ничего подобного. Оригинальное определение Лисков я узнал уже после определения Мартина (и забыл сразу же как прочитал, потому что определение Мартина проще запоминается).
Кстати, если разобраться — то как раз оригинальное определение Лисков несет неоднозначность, поскольку в нем не говорится какие именно свойства в нем рассматриваются: мгновенные (т.е. инварианты) или свойства поведения объекта.
В первом случае наследование мутабельного класса от иммутабельного не является нарушением LSP, ведь иммутабельность не является инвариантом типа!
Ах да, а еще оригинальный принцип подстановки Лисков не применим к интерфейсам, поскольку у интерфейса не может быть своих доказуемых свойств.
Кстати, если разобраться — то как раз оригинальное определение Лисков несет неоднозначность, поскольку в нем не говорится какие именно свойства в нем рассматриваются: мгновенные (т.е. инварианты) или свойства поведения объекта.
Говорится и очень подробно, как и про инварианты, так и про свойства поведения (она называет это constraints и очень подробно и формально определяет).
Ах да, а еще оригинальный принцип подстановки Лисков не применим к интерфейсам, поскольку у интерфейса не может быть своих доказуемых свойств.
У Лисков как бы вообще нету ни классов, ни интерфейсов. У нее типы и подтипы (а интерфейс, это очевидно тип) с контрактами. Если конкретный язык не позволяет задавать предусловия и постусловия для интерфейса — это не означает, что их можно игнорировать.
В первом случае наследование мутабельного класса от иммутабельного не является нарушением LSP, ведь иммутабельность не является инвариантом типа!
Как бы из предыдущего следует, что если программист написал в комментарии, что класс иммутабельный — это уже constraint и его надо соблюдать. Даже если еще не существует кода, который бы мог этот constraint нарушить.
Говорится и очень подробно, как и про инварианты, так и про свойства поведения (она называет это constraints и очень подробно и формально определяет)...
… в своей статье, но не в приведенном определении. То есть для того чтобы просто понять определение Лисков — надо идти и читать первоисточники, определение же Мартина более самодостаточное.
У Лисков как бы вообще нету ни классов, ни интерфейсов. У нее типы и подтипы (а интерфейс, это очевидно тип) с контрактами.
То есть определение Мартина более приближено к практическому программированию?
Как бы из предыдущего следует, что если программист написал в комментарии, что класс иммутабельный — это уже constraint и его надо соблюдать.
А если программист написал в комментарии, что класс может быть изменяемым, но реализация таковой не является?
Будет ли в таком случае иммутабельность provable property?
приведенном определении
Приведенном где и кем?
То есть определение Мартина более приближено к практическому программированию?
Не понимаю из чего сделан такой вывод. Мы не обсуждали применимость тех или иных определений к практическому программированию. Мы обсуждали их корректность и полноту, из чего и следует применимость. Если вы по Мартиновскому определению сами наследуете мутабельный класс от иммутабельного — это уже о нем говорит все что нужно знать.
А если программист написал в комментарии, что класс может быть изменяемым, но реализация таковой не является?
У вас контракт зависит от реализации?
Извините, но я, пожалуй, перестану тратить свое время.
У вас контракт зависит от реализации?
Нет, у вас. Нет, но меня смущает слово provable.
Приведенном где и кем?
В Википедии.
Мы не обсуждали применимость тех или иных определений к практическому программированию. Мы обсуждали их корректность и полноту, из чего и следует применимость.
Я утверждаю, что оба корректны и полны, но определение Мартина проще для понимания без специальных знаний.
Если вы по Мартиновскому определению сами наследуете мутабельный класс от иммутабельного
Пожалуйста, читайте мои сообщения внимательнее, чтобы не читать в них того чего я не писал.
LSP: The Liskov Substitution Principle
Имеет сложное математическое определение, которое можно заменить на: Функции, которые используют базовый тип, должны иметь возможность использовать подтипы базового типа, не зная об этом.
Мне кажется, что это определение вводит в заблуждение. Это скорее всего применимо к полиморфизму и ducktyping. Да и про функции в определении принципа нет упоминания.
Речь идет о типах: подтипы базовых типов не должны изменять свойства(как корректность) программы.
Например, если есть тип «драйвер базы данных», то подтип «драйвер базы данных с резервным копированием» отвечает принципу, а подтип «драйвер базы данных который всегда отбрасывает данные с невалидными полями» может и не подходить. Тут все зависит от рамок «корректности».
Пришел за откровением, часто пытаюсь объяснить эти принципы на пальцах. К сожалению, очередная из многих статей, объясняющих что такое ООП, SOLID…
Начинающему студенту / программисту все это пустой звук, можно зазубрить но тяжело осознать.
Понимание что такое и главное зачем нужно ООП приходит намного позже “начала использования классов”. Понимание SOLID приходит с опытом разработки, запуска и поддержки проектов.
Полное осознание приходит уже после того, как вы вовсю используете эти принципы.
В свое время именно его примеры вызвали тот самый эффект «Ага! Вот оно как!». Может и вам подойдет, как референс для новичков?
А вот «Принципы, паттерны и методики гибкой разработки на языке C#» я бы рекомендовал вдумчиво/осторожно читать. Местами она очень странная.
На каждом проекте люди играют разные роли (actor): Аналитик, Проектировщик интерфейсов, Администратор баз данных. Естественно, один человек может играть сразу несколько ролей. В этом принципе речь идет о том, что изменения в модуле может запрашивать одна и только одна роль. Например, есть модуль, реализующий некую бизнес-логику, запросить изменения в этом модуле может только Аналитик, но никак не DBA или UX.
SRP подразумевает не подчинение чьи-то требованиям, а манипуляция только одним актором.
«Бизнес-логика», кто бы её не запрашивал, есть в любом алгоритме. Она и есть «любой алгоритм» по определению. И в SRP речь идет не о том, что попросить поменять алгоритм может только кто-то один, а то, что модуль реализует алгоритм в отношении одной и только одной сущности. Все канонические примеры как раз показывают, что для соблюдения SRP в алгоритме не должно присутствовать действий над другими сущностями.
Иными словами для реализации действий с несколькими сущностями мы объявляем обобщенную мета-сущность, в которой реализуем алгоритм с вызовом соответствующих методов каждой сущности. Если нам нужно поменять этот мета-алгоритм, мы меняем локику этой мета-сущности. Если нужно поменять алгоритм модификации какой-то конкретной сущности, то лезем в код этой конкретной сущности, ничего не зная и даже не подозревая, что эта сущность может участвовать как составная в каком-то более общем алгоритме.
Вы читали первоисточники, которые указаны в статье?
Что есть Работник и метод save? Из-за архитектурного разделения на сущность и хранилище, метод save является частью хранилища, для которого Работник используется лишь как объект, который надо сохранить. Поэтому архитектурно неправильно делать метод save в Работнике, который лишь представляет собой структурированные данные.
То же самое касается и других взаимоотношений: Работник и отчет отработанных часов, Работник и расчет зарплаты. Эти методы используют Работника как источник первичных данных для расчета, а не для реализации внутренней бизнес-логики самого Работника. Поэтому эти методы должны реализовываться в отдельных сущностях, а не в самом Работнике.
Пример с ролями в компании, которые могут запрашивать бизнес-логику, мне не кажется полноценным. Тот же расчет зарплаты использует отчет по отработанным часам в качестве входных данных. И вполне нормально, если этому отчету нужна другая реализация данного отчета по отработанным часам, то реализовать эту вторую логику в «отчетах», дав на выбор, какую реализацию отчета использовать. А не завязываться исключительно на роли и кто что запросил, то там и реализуем.
Хорошая IT система представляет собой конфигуратор из различной функциональности. Где запросы внешнего пользователя удовлетворяются соответствующей конфигурацией.
Не знаю, как у «того», но, например, у меня основная претензия к SOLID в том, что их практически невозможно использовать как руководство «как надо делать», пока ты не созрел до того, чтобы их понять. А когда созрел, такое руководства тебе уже и не нужны.
И странно, что общественность игнорирует другие четрые буквы: GRASP. Значительно приземленнее и практичней.
SRP:
Мартин пишет не о участниках проекта, а о стейкхолдерах (на картинках — CFO, COO, CTO и ответственности типа «рассчитать зарплату», «отчитаться о рабочих часах», etc). То есть речь совсем не о DBA, а пользователях и заказчиках системы и их бизнес-процессах. И с этой точки зрения определение имеет смысл, «генерировать отчет для CFO» — это вполне себе ответственность. А вот причем здесь DBA и почему он диктует компонентам ответственности — совершенно непонятно.
OCP:
В книжке Мартин оперирует классами и компонентами. Слово «артефакт» используется только в контексте исходной формулировки принципа от Бертрана Мейера от 1988 года, и, очевидно, что это не jar и не dll.
LSP:
Здесь больше нет старой формулировки от Мартина. Только исходная формулировка от Лисков.
Сразу за ней идет раздел под названием GUIDING THE USE OF INHERITANCE и старая добрая проблема square/rectangle. И дальше уже про интерфейсы и их реализации.
Круто, что хоть кто-то открыл эту книжку =)
Еще советую посмотреть видео. Их можно найти.
SRP
DBA тут при том, что есть шлюз к базе, и актором, запрашивающим изменения будет DBA.
То, что вы привели — действительно цитата из книжки, я же привел свою трактовку.
OCP
Определение из статьи — цитата из книжки. Трактовка взята из соответствующего видео с cleancoders.com. Артефакт — это файл, содержащий класс в текстовой форме, dll, и т.п.
LSP
В основном, эта часть опять взята с cleancoders.com
Вы приводите пример в четвертом абзаце с Circle и Shape. Естественно что Circle нельзя заменить Shape, но этого и не было написано в принципе:
Функции, которые используют базовый тип, должны иметь возможность использовать подтипы базового типа, не зная об этом.
Тут написано о возможности использовать подтип. То есть заменим
List<Shape>
на List<Circle>
и никто не умрет, так как Circle
точно имеет все методы Shape
, но не наоборот. Вы же хотели сделать наоборот, что естественно нарушает этот принцип и не логично по своей природе.Тут речь про то, что модели не обязаны реализовывать отношения объектов реального мира.
Т.е. в реальном мире список окружностей является подтипом списка фигур, но в программной модели с изменяемыми списками это не так.
но в программной модели с изменяемыми списками это не так.
Достаточно сделать список неизменяемым — и он станет ковариантным.
В реальном мире окружностей вообще нет :) С другой стороны, список окружностей является подмножеством, а не подтипом списка фигур. Это у списка окружностей может быть подтип список фигур, а не наоборот. Наследование расширяет множество значений и допустимых операций супертипа, а не сужает его. Вы, конечно, можете его сузить в реализации, но это нарушит LSP :)
Чем это его нарушит? В список фигур мы можем добавить окружность? Можем. Из списка фигур мы можем получить окружность или другие фигуры — её подтипы? Можем.
Из списка фигур мы можем получить любую фигуру, в то время как из списка окружностей — только окружность.
var list = new List<Shape>(); // Список фигур
list.Add(new Reclangle()); // Разрешено, потому что прямоугольник - фигура
func(list);
void func(List<Circle> list) { // Функция принимает список окружностей
Circle c = list[0]; // БАМ! Там лежал прямоугольник.
}
// БАМ! Там лежал прямоугольник.
А прямоугольник у меня подтип окружности :)
List<Shape>
на List<Circle>
заменять точно так же нельзя, потому что List<>
— инвариантный обобщенный тип.
Смотрите что было бы, если бы было можно:
void func(List<Shape> list) {
list.Add(new Rectangle());
}
var list = new List<Circle>(); // В Java будет new ArrayList<Circle>();
foo(list);
Circle c = list[0]; // БАМ! Прямоугольник - не круг.
Именно!
class Man {
public void eat() { ... }
}
class Asian extends Man {
//...
}
class European extends Man {
//...
}
// метод для всех
// Каждый человек может есть, поэтому
// Man можем заменить на Asian или European
void method(Man man) {
man.eat();
}
void func(List<Shape> list) {
list.Add(new Rectangle());
list.Add(new Circle());
}
// Всё ок добавили Rectangle и Circle
// В дальнейшем коде пользуемся лишь теми методами которые доступны Shape,
// т.е. вызов list[0].getRadius() будет нарушением принципа LSP, т.к. радиус
// только у окружности или круга.
// Пользуемся методами доступными всем, например абстрактный метод draw().
// Каждая фигура может "нарисоваться". Вот и соблюдение принципа.
Спасибо за статью, такой вопрос по LSP. Вы сказали что
И есть потомок DoubleStack, который дублирует добавляемые элементы. Естественно, класс DoubleStack нельзя использовать вместо Stack.
С этим все понятно, но хотелось бы узнать, кто нарушает в таком случае принцип LSP, тот кто его так реализовал или тот кто его так использует? Если первый вариант правильный то для ООП языков это означало бы — избегайте глобальных абстракций (List, Hash, etc). Спасибо.
Виноват тот, кто реализовал DoubleStack.
избегайте глобальных абстракций (List, Hash, etc)
Несколько странный вывод =)
Хорошо бы пользоваться соответствующими интерфейсами, а для их выбора поможет ISP.
На самом деле не факт, что LSP нарушается, если в контракте Stack не заявлено, что pop добавляет один и только один элемент, увеличивая length на 1. Если клиенты Stack не делают предположений как работает стэк, не ожидают, что
l = stack.length();
stack.push(a);
stack.push(b);
assert(l + 2 === stack.length();
assert(b === stack.pop());
assert(a === stack.pop());
то нарушения нет.
Хочу немного дополнить. В будущем может оказаться, что НЕ ВСЕ части проекта используют одну и ту же бд, одну и ту же субд, один и тот же принцип хранения данных. То есть не то, чтобы мы что-то меняли глобально, но внезапно появилась подсистема, в которую часть данных будет поступать из 1С…
Смотря что вы называете разработкой. Написать одну версию "под ключ" по подробному ТЗ, сдать и забыть, а если придётся дорабатывать, то вторую версию писать с нуля — тут эти принципы могут удорожить разработку.
Классический пример нарушения. Есть базовый класс Stack, реализующий следующий интерфейс: length, push, pop. И есть потомок DoubleStack, который дублирует добавляемые элементы. Естественно, класс DoubleStack нельзя использовать вместо Stack.
Вообще-то может, и никаким нарушением принципа это не будет.
Что в этом случае поламается? То, что зависело от никак незаконтрактованной логики использования базового класса.
Вот если у нас законтрактован результат работы цепочки вызовов push и pop, то и потомок обязан будет выполнять этот контракт. Иначе он не потомок. Позволяют ли средства языка описывать подобные контракты — это другой вопрос. Как частное решение для классов, в контракте которых подразумевается конкретная логика их использования, языки могут предоставлять возможность их финализации, на уровне синтаксиса запрещая создавать от них потомки.
Что такое модули верхних уровней? Как определить этот уровень? Как оказалось, все очень просто. Чем ближе модуль к вводу/выводу, тем ниже уровень модуля. Т.е. модули, работающие с BD, интерфейсом пользователя, низкого уровня. А модули, реализующие бизнес-логику — высокого уровня.
не совсем
нижний уровень это взаимодействие: ввод-вывод, API, ...
почему нижний? потому что это привязано к контексту и значит тут наиболее часто изменяемый код
потом привязка к вариантам использования, т.е. блокам возможных входных сообщений и контекстов, это сервисный слой
самый верхний слой (домен) он не зависит от вариантов использования и самый редко изменяемый
итак, если хотите какую-то непрерывную меру то я бы выбрал "независимость от контекста и инструментов", чем меньше зависимость тем выше уровень
также можно использовать то что зависимость и управление идут рядом
если в бизнесе есть управляющий сервис, то он выше по абстракции
обслуживающие сервисы ниже по уровню абстракции и больше подвержены изменениям
SOLID