Есть проблема с описанием и толкованием принципов развития архитектуры SOLID (авторства Роберта Мартина). Во многих источниках дается их определение и даже примеры их использования. Изучая их и пробуя использованием примерить на себя, стабильно ловил себя на мысли, что не хватает объяснения магии их применения. И пытаясь увидеть внутренние шестеренки, понять — и для меня значит запомнить — разложил их по своим "терминам-полочкам". Хорошо если это будет полезно еще кому-нибудь.
Приступим "жонглировать полочками" вышеозначенного подхода проектирования.
-
Single Responsibility Principle (SRP) принцип единственной ответственности
Один участок кода должен меняться только в ходе реализации одной цели. Если участок кода реализует две задачи и меняется для разного использования, то следует продублировать этот участок по экземпляру для каждой цели. Это очень важно, потому что требует отступить от общепринятого принципа устранения дублирования.
Целью этого принципа является устранение неявно вносимых ошибок, получаемых из-за того, что в разработке для участка кода, процедуры, класса, компонента (далее для объединения этих понятий используется термин [компонент]) существуют следующие инварианты:
- [1] корректно написанный [компонент] обязательно используется и чаще несколько раз,
- [2] в каждом месте использования от [компонента] ожидается неизменное поведение приводящее к повторяемому результату,
- [3] при использовании [компонента] в нескольких местах результат должен удовлетворять каждому месту использования,
- если для одного из мест использования требуется изменение [компонента], а для другого места использования требуется прежнее поведение [компонента], то необходимо создание копии [компонента] с последующей её модификацией (или обобщение [компонента] дополнительными параметрами, обеспечивающих разное поведение),
- если есть места использования [компонента], которые не важны для текущей задачи, решаемой программистом, то ему очень легко забыть о проверке совместности с этими местами использования вносимого в этот [компонент] изменения.
Поэтому все места использования должны располагаться в зоне [Single Responsibility] единой ответственности, то есть изменяться и учитываться разом для любой решаемой программистом задачи).
Принцип относится как к участку кода, так и к компоненту, библиотеке, программе, комплексу программ, используемых в нескольких местах.
В многих источниках приводят пример класса с одной только "функцией" как идеал SRP и класс "божественного объекта", совмещающий все функции приложения, как антипаттерн. IMHO класс с одной только "функцией" это требование преждевременной оптимизации архитектуры кода, побуждающее на пустом месте писать множества классов (кодовых сущностей), при этом забывая, что отсутствие более одного места использования позволяет программисту быстрее оценить малое количество расположенного локально (в одном классе) взаимодействующего кода, чем анализировать внешние связи разрозненных кодовых сущностей, ответственных за свою "функцию". "Божественный объект" для крошечного приложения тоже вроде не сильный криминал — он позволяет запустить разработку: выделить все необходимые сущности и, записав их рядом, отделить от внешних объектов стандартной библиотеки и внешних модулей (создать живую клеточку и обособить её мембраной). В процессе роста и развития проекта существует множество приемов помогающих следовать SRP, один из них разделения на классы и минимизация количества "функций", за которые каждый класс отвечает (деление клеточек и их специализация в организме).
Здесь хотелось бы выписать набор приемов поддержания SRP, но эта работа пока не завершена (надеюсь "руки дойдут"). Из очевидных областей, где можно поискать эти приемы:
- паттерны проектирования;
- использование разных специализированных веток компонента в отличие от создания компонента удовлетворяющего всем способам применения (fork на GitHub).
-
Open-Closed Principle (OCP) принцип открытости/закрытости
Развитие кода оптимально планировать так, чтобы для реализации программистом новых задач требовалось добавлять новый код, а старый код при этом в изменениях не нуждался. Код должен быть открыт (Open) для добавления и закрыт (Closed) для изменения.
Целью для этого принципа является минимизация трудозатрат и устранение неявно вносимых ошибок, получаемых из-за того, что в разработке существуют следующие инварианты:
- [1], [2], [3], описанные ранее,
- для реализации новой задачи программист может добавить новые [компоненты] или изменить поведения старых [компонентов],
- добавление [компонента] требует проверки в месте нового использования, и порождает затраты времени программиста
- обусловленное новой задачей изменение поведения [компонента] требует проверки в месте нового использования и во всех местах старого использования, что также порождает затраты времени программиста, а в случае опубликованного [компонента] работу всех программистов, использовавших [компонент].
- вариант реализации новой задачи целесообразно выбирать минимизируя затраты времени программиста.
Чаще в практике разработки программного обеспечения затраты добавления гораздо меньше затрат изменения, что делает очевидной пользу использования [Open-Closed] принципа. При этом существует масса приемов поддержания архитектуры программы в состоянии, когда реализация новой задачи сводится только к добавлению [компонентов]. Эта работа с архитектурой тоже требует затрат времени программиста, но как показывает практика в крупных проектах гораздо меньших чем использование подхода изменений старых процедур. И, конечно, это описание разработки — идеализация. Почти не бывает реализации задачи только добавлением или только изменением. В реальных задачах применяется смесь этих подходов, но OCP подчеркивает пользу в использовании подхода добавления.
И здесь хотелось бы выписать набор приемов поддержания OCP. Из очевидных областей, где можно поискать эти приемы:
- паттерны проектирования;
- библиотеки dll и варианты их распространения, обновления и развития функционала;
- развитие COM библиотек и объектов в них;
- развития языков программирования и поддержка ранее написанного кода;
- развите законодательной системы государства.
-
Liskov Substitution Principle (LSP) принцип подстановки Барбары Лисков
Данный принцип ограничивает использование расширения базового интерфейса [базы] реализацией, закрепляя что каждая реалицация базового интерфейса должна иметь поведение как базовый интерфейс. При этом базовый интерфейс закрепляет поведение ожидаемое в местах его использования. И наличие в поведении реализации отличия от ожидаемого поведения, закрепляемого базовым интерфесом, приведет к возможности нарушения инварианта [2].
Данный принцип основывается и уточняет прием проектирования, основанный на абстрагировании. В этом подходе вводится абстракция — закрепляется некоторое базовое свойства и поведение, характерные множеству ситуаций. Например, [компонент-процедура] "Передвинуть в предыдущую позицию" для ситуаций: "Курсор в тексте", "Книга на полке", "Элемент в массиве", "Ноги в танце" и др. И за этим [компонентом] закрепляются (часто житейским опытом и без формализации) некоторые предпосылки и поведение, например: "Наличие передвигаемого объекта", "Повтор несколько раз", "Наличие порядка элементов", "Наличие закрепленных позиций элементов". LSP требует чтобы при добавлении новой ситуации использования для [компонента] выполнялись все предпосылки и ограничения базы. И ситуация "крупица в банке сахара" не может быть описана данной абстракцией, хотя у крупицы, конечно, есть позиция, есть позиции в которых крупица пребывала ранее, и есть возможность её в них передвинуть — отсутствуют лишь закрепленные позиций элементов.
Целью для этого принципа является устранение неявно вносимых ошибок, получаемых из-за того, что в разработке существуют следующие инварианты:
- [1], [2], [3], описанные ранее,
- базовая [процедура] описывает поведение, которое является полезным в большом количестве ситуаций, задавая ограничения, требуемые для ее применимости,
- разработанная [процедура] реализации базы должна выполнять все её ограничения, включая тяжело отслеживаемые подразумеваемые (предоставленные неформально).
Очень часто для описания этого принципа приводят пример с Прямоугольном ([базой]) и Квадратом (реализацией). Ситуация
class CSquare : public CRectangle
. В [базе] вводят операции работы с шириной и высотой (Set(Get)Width, Set(Get)Height). В реализации CSquare эти Set-операции вынуждены менять оба размера объекта. Мне всегда не хватало пояснения, что "неформально" в [базе] задается следующее ограничение: "возможность независимого использования Width, Height". В реализации CSquare оно нарушается, и в местах использования простая последовательность действий, основанная на использовании этой независимости:r.SetWidth(r.GetWidth()*2); r.SetHeight(r.GetHeight()*2)
— для реализации CSquare увеличит оба размера в 4 раза, вместо 2 раз предполагаемых для CRectangle.IMHO данный принцип указывает на сложность отслеживания подобных неформальных ограничений, что при огромной полезности и большой частоте использования подхода разработки "база-реализация" требует особого внимания.
-
Interface Segregation Principle (ISP) принцип разделения интерфейсов; Dependency Inversion Principle (DIP) принцип инверсии зависимости
Эти два принципа очень близки по области своих требований. Оба неявно подразумевают полезность использования минимально возможного базового интерфейса, как инструмента взаимодействия двух [компонентов]: "клиент" и "сервер" — эти названия выбраны просто для идентификации. При этом общая информация, используемая [компонентами], сосредотачивается в базовом интерфейсе. Один [компонент] ("сервер") выполняет реализацию базового интерфейса, другой [компонент] ("клиент") обращается к этой реализации.
Целью для этих принципов является минимизация зависимостей компонентов, позволяющая производить независимые изменения их кода, если он не меняет базовый интерфейс. Независимость изменения компонентов уменьшает сложность и трудозатраты, если компоненты выполняют требования принципа SRP. Подобный подход возможен, потому что в разработке существуют следующие инварианты:
- [1], [2], [3], описанные ранее,
- каждый [компонент] заложенным в нем поведением формирует ограничения своего использования,
- в каждом месте использования [компонента] могут быть задействованы все его ограничения,
- базовый [компонент] следствием из определения имеет меньшую сложность и количество ограничений чем [компонент] реализация,
- любое изменение [компонента] изменяет его ограничения и требует проверки всех мест его использования, что порождает затраты времени программиста,
- места использования базового [компонента] не требуют проверки после внесения изменений в [компонент] реализацию.
При этом понятно что "размер" базового интерфейса целесообразно минимизировать, откидывая не используемый функционал и ограничения, тем самым меньше ограничивая [компонент] реализацию по принципу (LSP)
Принципом ISP подчеркивается необходимость разделения (Segregation) интерфейса "сервера", если не весь его публикуемый функционал используется данным "клиентом". При этом выделяется только требуемая клиенту [база] и обеспечивается минимизация совместно ограничивающей информации.
И здесь хотелось бы выписать набор приемов поддержания DIP. Из очевидных областей, где можно поискать эти приемы:
- разделение описание класса на публичные и приватные части (и другие принципы ООП),
- описание взаимодействия с динамической библиотекой ограниченным набором функций и дескрипторов объектов,
- использование картотеки как интерфейса доступа к книжной библиотеки.
Возвращаясь к заголовку, объясню почему выбрано "не понимать". Отрицание добавлено для того, чтобы подчеркнуть ошибками выстраданный и очень IMHO полезное правило. Лучше не понимать и потому не использовать технологию, чем понимать неправильно, принимать на веру, тратить на применение технологии свои ресурсы и в результате не получать при этом никакого полезного выхлопа кроме самоуспокоения и возможности хвастовства о причастности к модной технологии.
Спасибо за внимание.
Автор: ai_borisov