Есть проблема с описанием и толкованием принципов развития архитектуры SOLID (авторства Роберта Мартина). Во многих источниках дается их определение и даже примеры их использования. Изучая их и пробуя использованием примерить на себя, стабильно ловил себя на мысли, что не хватает объяснения магии их применения. И пытаясь увидеть внутренние шестеренки, понять — и для меня значит запомнить — разложил их по своим "терминам-полочкам". Хорошо если это будет полезно еще кому-нибудь.


image


Приступим "жонглировать полочками" вышеозначенного подхода проектирования.


  • 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 полезное правило. Лучше не понимать и потому не использовать технологию, чем понимать неправильно, принимать на веру, тратить на применение технологии свои ресурсы и в результате не получать при этом никакого полезного выхлопа кроме самоуспокоения и возможности хвастовства о причастности к модной технологии.


Спасибо за внимание.

Комментарии (12)


  1. crea7or
    23.03.2019 12:46

    «Один участок кода должен меняться только в ходе реализации одной цели.» это вообще про что? Ощущение, что код сам по себе должен меняться, рефлексией или ещё как. Но это же не про solid.


    1. ai_borisov Автор
      23.03.2019 17:31
      +1

      Подразумевалось: "меняться программистом"


  1. rumyancevpavel
    23.03.2019 13:10

    Несколько замечаний автору:
    1) Роберт Мартин заостряет внимание читателя на различии между software design и software architecture. SOLID — имеет большее отношение к дизайну систем, а не архитектуре как заявлено в заголовке. Непонимание этого — очень большое упущение.
    2) Чтение подобной литературы не в оригинале, не на английском так же ведет к не верной трактовке написанного.


    1. ai_borisov Автор
      23.03.2019 18:19

      Да, переводы вносят смысловые "потери". Изредка сталкивался с последствиями этих потерь в рабочем процессе, но написать и тем более опубликовать этот разбор заставила англоязычная книга русского автора, в которой эти потери для принципов SOLID закреплены переносом на язык оригинала. Слово "архитектура" появилось из названия русского перевода упоминаемой (фото обложки) книги Р. Мартина: "Чистая архитектура".


    1. kolyaflash
      23.03.2019 23:02
      +3

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

      Р. Мартин, «Чистая архитектура»


  1. Xtray
    24.03.2019 12:11

    Текст читать тяжело: куча вставок (зачем?), сложные [предложения], по поводу некоторых терминов (инвариант) вообще есть сомнения в правильности (их) использования.
    Мне кажется, такие статьи только усложняют [задачу].
    Знаки препинания конечно же в данном случае не несут ни малейшей сколь бы то ни было заметной пользы.


    1. ai_borisov Автор
      24.03.2019 17:48

      Спасибо, согласен. Попробую исправлять.


      1. ghost404
        24.03.2019 19:35

        А ещё есть такая штука как подзаголовки. Рекомендую


  1. ghost404
    24.03.2019 20:13
    +1

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


    Зачем это мудрёное усложнение? Там же всё просто. Из всех 5 принципов, только LSP сложен для восприятия, и то вы как-то коряво его объяснили.


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


    И зачем вы объединили ISP и DIP? Это разные принципы. У них из общего только то, что они входят в SOLID. К слову, SOLID это про зависимости.


    Рекомендую к прочтению https://github.com/jupeter/clean-code-php или форк на русском https://github.com/peter-gribanov/clean-code-php


  1. muhaa
    25.03.2019 00:48

    Мне понравилось. Всегда плохо понимал как использовать все эти SOLID и прочее на практике, но то что написано здесь понимаю. Для того, чтобы получить хороший код нужно изобрести некую подходящую схему из абстракций и писать код в ее рамках. Если получается выделить в этой схеме достаточно независимые элементы и описывать их в независимых частях кода, то это хорошая схема. Хорошо, если дальше элементы добавляются, плохо если их приходится переделывать (значит изначально схема была не удачной). Хорошо, если элементы схемы четкие и понятные, плохо если не всегда ясно что от них ожидать. Хорошо, если элементы удается изолировать друг от друга заставив их общаться на одном языке.
    Эти мысли понятны, это все помогает справиться со сложностью и работает одинаково и в общей архитектуре и в деталях, хоть при объектном, хоть при функциональном, хоть при процедурном подходе.
    Обычно когда пишут о подобных вопросах, приводят всякие странные тривиальные примеры, из которых вообще не ясно как предлагается решать главную проблему — как справиться со сложностью.


    1. ai_borisov Автор
      26.03.2019 00:29

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


      1. muhaa
        26.03.2019 11:58

        Судя по комментариям выше вас ждут большие сложности на этом пути. Попробую выразить мысль, используя в качестве аналогии изучение математики. Большинство людей изучая математику будут скрупулезно изучать идеи, термины и отрабатывать приемы на задачах. При этом они ни на минуту не задумаются почему это вообще работает и что это вообще все значит.
        Допустим условно, что некто, не обладающий мощью и авторитетом великих пытается объяснить как пользоваться некой теорией из математики, физики или программирования в контексте указанных вопросов (почему работает и что все это значит). Тогда реакция читателей будет примерно следующей:
        1. Вы слишком много на себя берете, вы думаете, что понимаете эти теории лучше чем их авторы? Что это вообще все за муть? (потому что даже авторы теорий таких широких обобщений не делали).
        2. Вы невежественны в терминологии и понимании канонических истин, искажаете все, несете ересь и еще беретесь нас учить. (потому, что автор концентрируется на общих принципах и забил на ловлю блох и каноническую схоластику, которой все ждут).
        3. Вы слишком все усложняете, на самом деле все можно объяснить проще (потому что автор берет на себя более амбициозную задачу, чем просто объяснить как пользоваться принципами, а большинство его читателей сталкиваются только с очень тривиальными задачами, в которых они скрупулезно применяют разные принципы не понимая зачем и получая больше проблем чем пользы).
        4. Вы изобретаете какую-то свою теорию (потому что отчасти это правда).
        5. Вы не понимаете истинного значения этих принципов (потому что автор не работал с теми же задачами и в той же корпоративной среде что большинство читателей).
        В итоге автор получит кучу негатива и пару положительных отзывов от философски настроенных невежд, вроде меня.