Принципы хорошей архитектуры

SOLID

SOLID - мнемонический акроним, для первых пяти принципов, названных Робертом Мартином в начале 2000-х, которые означали пять основных принципов объектно-ориентированного(и не только) программирования и проектирования.

Принцип единственной ответственности (Single responsibility)

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

Принцип открытости/закрытости (Open-closed)

Программные сущности должны быть открыты для расширения и закрыты для изменения.

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

Принцип подстановки Барбары Лисков (Liskov substitution)

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

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

В отличие от других принципов, которые определены очень свободно и имеют массу трактовок, принцип подстановки имеет ряд формальных требований к подклассам, а точнее к переопределённым в них методах:

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

Замена объектов их наследниками не должна нарушать работу программы.(У наследника - типы принимаемых параметров должны быть не уже, возвращаемых не шире + должны сохраняться инварианты).

Принцип разделения интерфейса (Interface segregation)

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

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

Принцип инверсии зависимостей (Dependency Invertion)

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

Предпочитайте композицию наследованию класса

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

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

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

Анемичная модель - антипаттерн

Предпочитайте нормальную модель бизнес логики(иногда называют Rich Domain Model), а не процедурную, анемичную модель с DTO вместо полноценных сущностей. Анемичная модель не имеет ничего общего с ООП и ее следует рассматривать как неудачный пример процедурного программирования.

Дополнительно:

Иммутабельность

Иммутабельные данные( объекты например) - те, которые нельзя изменить. Примерно как числа. Число просто есть, его нельзя поменять. Также и иммутабельный массив — он такой, каким его создали, и всегда таким будет. Если нужно добавить элемент — придется создать новый массив.

Преимущества неизменяемых структур:

  • Безопасно разделять ссылку между потоками
  • Легко тестировать
  • Легко отследить жизненный цикл (соответствует data flow)
  • Легче читать и поддерживать код
  • Лучшая инкапсуляция

Иммутабельность полезна в случаях, когда создаётся кусок данных (объект), который используется в нескольких местах и/или в разных потоках. Тогда вероятность того, что такой объект ВНЕЗАПНО поменяется, равна нулю. В частности, иммутабельный объект всегда потокобезопасен и реентерабелен без дополнительных усилий. В отличие от мутабельных объектов.

Закон Деметры

Говоря упрощённо, каждый программный модуль:

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

Аналогия из жизни: Если Вы хотите, чтобы собака побежала, глупо командовать её лапами, лучше отдать команду собаке, а она уже разберётся со своими лапами сама.

Основной идеей является то, что объект должен иметь как можно меньше представления о структуре и свойствах чего угодно (включая собственные подкомпоненты).

Общее описание правила: Объект A не должен иметь возможность получить непосредственный доступ к объекту C, если у объекта A есть доступ к объекту B и у объекта B есть доступ к объекту C.

Более формально, Закон Деметры для функций требует, что метод М объекта О должен вызывать методы только следующих типов объектов:

  • собственно самого О
  • параметров М
  • других объектов, созданных в рамках М
  • прямых компонентных объектов О
  • глобальных переменных, доступных О, в пределах М

Практически, объект-клиент должен избегать вызовов методов объектов, внутренних членов, возвращенных методом объекта-сервиса.

GRASP

GRASP (general responsibility assignment software patterns — общие шаблоны распределения обязанностей; также существует английское слово "grasp" — «контроль, хватка») — шаблоны, используемые в объектно-ориентированном проектировании для решения общих задач по назначению обязанностей классам и объектам.

1. Информационный эксперт (Information Expert) Шаблон определяет базовый принцип распределения обязанностей:

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

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

Локализация же обязанностей, проводимая согласно шаблону:

  • Повышает:
  • Инкапсуляцию;
  • Простоту восприятия;
  • Готовность компонентов к повторному использованию;
  • Снижает: степень зацеплений.

2. Создатель (Creator)

  • Класс должен создавать экземпляры тех классов, которые он может:
  • Содержать или агрегировать;
  • Записывать;
  • Использовать;
  • Инициализировать, имея нужные данные.

Так применяется шаблон «Информационный эксперт» (смотрите пункт №1) в вопросах создания объектов.

Альтернатива — шаблон «Фабрика» (создание объектов концентрируется в отдельном классе).

3. Контроллер (Controller)

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

Может представлять собой:

  • Систему в целом;
  • Подсистему;
  • Корневой объект;
  • Устройство.

4. Слабое зацепление (Low Coupling)

«Степень зацепления» (сопряжения[2]) — мера неотрывности элемента от других элементов (либо мера данных, имеющихся у него о них).

«Слабое» зацепление — распределение обязанностей и данных, обеспечивающее взаимную независимость классов. Класс со «слабым» зацеплением:

  • Не зависит от внешних изменений;
  • Прост для повторного использования.

5. Высокая степень связности (High Cohesion)

Предметные области следует разделять по классам.

Связность класса — мера подобия предметных областей его методов:

«Высокая» степень — сфокусированные подсистемы (предметная область определена, управляема и понятна);

«Низкая» степень — абстрактные подсистемы. Затруднены:

  • Восприятие;
  • Повторное использование;
  • Поддержка;
  • Устойчивость к внешним изменениям.

6. Полиморфизм (Polymorphism)

Устройство и поведение системы:

  • Определяется данными;
  • Задано полиморфными операциями её интерфейса.

Пример: Адаптация коммерческой системы к многообразию систем учёта налогов может быть обеспечена через внешний интерфейс объектов-адаптеров (смотрите также: Шаблон «Адаптеры»).

7. Чистая выдумка (Pure Fabrication)

Не относится к предметной области, но:

  • Уменьшает зацепление;
  • Повышает связность;
  • Упрощает повторное использование.

«Pure Fabrication» отражает концепцию сервисов в модели проблемно-ориентированного проектирования.

Пример задачи: Не используя средства класса «А», внести его объекты в базу данных.

Решение: Создать класс «Б» для записи объектов класса «А» (смотрите также: «Data Access Object»).

8. Посредник (Indirection)

См. также: Посредник (шаблон проектирования)

Слабое зацепление между элементами системы (и возможность повторного использования) обеспечивается назначением промежуточного объекта их посредником.

Пример: В архитектуре Model-View-Controller, контроллер (англ. controller) ослабляет зацепление данных (англ. model) за их представление (англ. view).

9. Устойчивость к изменениям (Protected Variations)

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

Признаки плохого проекта

  • Закрепощённость: система с трудом поддается изменениям, поскольку любое минимальное изменение вызывает эффект "снежного кома", затрагивающего другие компоненты системы.
  • Неустойчивость: в результате осуществляемых изменений система разрушается в тех местах, которые не имеют прямого отношения к непосредственно изменяемому компоненту.
  • Неподвижность: достаточно трудно разделить систему на компоненты, которые могли бы повторно использоваться в других системах.
  • Вязкость - сделать что-то правильно намного сложнее, чем выполнить какие-либо некорректные действия.
  • Неоправданная сложность: проект включает инфраструктуру, применение которой не влечёт непосредственной выгоды.
  • Неопределенность: исходный код трудно читать и понимать. Недостаточно четко выражено содержимое проекта.