Архитектурные принципы
Совет
Это фрагмент из книги, архитектор современных веб-приложений с ASP.NET Core и Azure, доступный в документации .NET или в виде бесплатного скачиваемого PDF-файла, который можно читать в автономном режиме.
"Если бы строители возводили здания так же, как программисты пишут программы, первый же дятел уничтожил бы всю цивилизацию".
- Джеральд Вайнберг
При разработке архитектуры и проектировании программных решений важно учитывать удобство поддержки. В этом разделе описываются принципы принятия решений о выборе архитектуры, которые помогут вам строить прозрачные и удобные в поддержке приложения. В общем случае эти принципы рекомендуют создавать приложения, состоящие из слабо связанных с другими частями приложения компонентов, взаимодействие между которыми осуществляется с использованием явных интерфейсов или систем обмена сообщениями.
Общие принципы проектирования
Разделение задач
Основополагающим принципом разработки является разделение задач. Этот принцип подразумевает разделение программного обеспечения на компоненты в соответствии с выполняемыми ими функциями. Рассмотрим пример приложения, в котором используется логика выбора отображаемых пользователю элементов, а также осуществляется форматирование этих элементов для максимально эффективного показа. Выбор форматируемых элементов должен быть отделен от самих функций форматирования, так как это разные задачи, не имеющие тесной связи.
С точки зрения архитектуры для соблюдения этого принципа при проектировании следует отделять бизнес-логику от инфраструктуры и функций пользовательского интерфейса. В идеальном случае бизнес-правила и логика должны размещаться в отдельном проекте, который не должен зависеть от других проектов в приложении. Такое разделение обеспечивает простоту тестирования бизнес-модели и ее совершенствование без тесной взаимосвязи с низкоуровневыми сведениями о реализации (это также помогает в том случае, если проблемы с инфраструктурой зависят от абстракций, определенных на бизнес-уровне). Разделение задач является одним из основных вопросов, который следует учитывать при использовании слоев архитектур приложения.
Инкапсуляция
Инкапсуляция отдельных частей приложения позволяет изолировать их друг от друга. Корректировка внутренней реализации компонентов и слоев приложения должна быть возможна без влияния на участников совместной работы при условии, что при этом не нарушаются внешние контракты. Правильно реализованная инкапсуляция позволяет получить слабо связанную модульную структуру приложения, поскольку объекты и пакеты можно с легкостью заменять альтернативными реализациями, если при этом сохраняется один и тот же интерфейс.
Инкапсуляция классов реализуется посредством ограничения доступа извне к внутреннему состоянию класса. Если внешнему субъекту требуется изменить состояние объекта, он должен использовать четко определенную функцию (или метод задания свойств) вместо того, чтобы напрямую получать доступ к закрытому состоянию объекта. Аналогичным образом, компоненты приложений и сами приложения должны предоставлять четко определенные интерфейсы, которые будут использоваться участниками совместной работы вместо того, чтобы допускать изменение их состояния напрямую. Такой подход позволяет свободно модернизировать внутреннюю структуру приложения со временем, не беспокоясь о том, что это может нарушить функционирование других участников совместной работы (при условии, что соблюдаются условия открытых контрактов).
Изменяемое глобальное состояние противоположно инкапсуляции. Значение, полученное из изменяемого глобального состояния в одной функции, не обязательно будет тем же в другой функции (или даже далее в той же функции). Проблемы с изменяемым глобальным состоянием являются одной из причин, по которым такие языки, как C#, поддерживают различные правила области, которые используются во всем коде — от инструкций до методов и классов. Стоит отметить, что архитектуры на основе данных, использующие для интеграции внутри приложений и между ними центральную базу данных, сами по себе могут зависеть от изменяемого глобального состояния, представленного базой данных. Для проектирования на основе домена и обеспечения чистой архитектуры прежде всего нужно понять, как инкапсулировать доступ к данным и избежать ситуации, при которой состояние приложения становится недопустимым в результате прямого доступа к его формату сохраняемости.
Инверсия зависимостей
Зависимость в приложении должна быть направлена в сторону абстракции, а не на детали реализации. При написании большинства приложений направление зависимостей времени компиляции задается в сторону времени выполнения. Это создает прямую схему зависимостей. То есть, если класс А вызывает метод класса Б, а класс Б вызывает метод класса В, то во время компиляции класс А будет зависеть от класса Б, а класс Б будет зависеть от класса В, как показано на рис. 4-1.
Рис. 4-1. Схема прямых зависимостей.
Применение принципа инверсии зависимостей позволяет модулю A вызывать методы абстракции, которые реализует модуль B. Это значит, что модуль A может вызывать модуль B во время выполнения, однако B будет зависеть от интерфейса, управляемого модулем A во время компиляции (таким образом, типовая зависимость времени компиляции инвертируется). Во время выполнения поток выполнения программы остается неизменным, однако при этом легко могут быть подключены новые реализации интерфейсов.
Рис. 4-2. Схема инвертированных зависимостей.
Инверсия зависимостей является важной частью процесса создания слабо связанных приложений, так как детали реализации могут описывать зависимости и реализовывать абстракции более высокого уровня, а не компоненты того же уровня. В результате получаются приложения с более высоким уровнем тестируемости, модульности и удобства в обслуживании. Практика внедрения зависимостей базируется на соблюдении принципа инверсии зависимостей.
Явные зависимости
Методы и классы должны явно требовать наличия всех совместно работающих объектов, которые необходимы для их корректного функционирования. Он называется принципом явных зависимостей. Благодаря конструкторам классов классы могут идентифицировать объекты, которые им необходимы для сохранения корректного состояния и правильного функционирования. Если определены классы, которые могут конструироваться и вызываться, но которые корректно работают только при наличии определенных глобальных или инфраструктурных компонентов, поведение таких классов будет не до конца прозрачным для клиентов. Контракт конструктора указывает клиенту на то, что ему требуются только заданные компоненты (если класс использует конструктор без параметров, не требуется ничего), однако во время выполнения выясняется, что объекту фактически требуется что-то еще.
При соблюдении принципа явных зависимостей ваши классы и методы будут прозрачны для клиентов, указывая все необходимые им для работы объекты. Благодаря соблюдению этого принципа код и контракты программирования станут более понятными, так как пользователи будут уверены, что, если предоставлены все требуемые в параметрах метода или конструктора объекты, во время выполнения такой метод или конструктор будет работать корректно.
Единственная обязанность
Принцип единственной обязанности применяется к объектно-ориентированному проектированию, но также может рассматриваться и как архитектурный принцип аналогично разделению задач. Этот принцип подразумевает, что объекты должны иметь только одну обязанность и только одну причину для изменения. В частности, единственным сценарием, в котором должен изменяться объект, является обновление способа выполнения объектом его единственной обязанности. Соблюдение этого принципа позволяет создавать модульные системы с меньшей степенью связанности, так как многие виды нового поведения можно реализовать в виде новых классов, не добавляя дополнительные обязанности к существующим. Добавление новых классов всегда более безопасно по сравнению с изменением существующих, поскольку в этот момент никакой код еще не зависит от новых классов.
В монолитном приложении принцип единственной обязанности может применяться на высоком уровне к слоям приложения. Обязанность представления должна оставаться в проекте пользовательского интерфейса, тогда как обязанность доступа к данным будет отнесена к проекту инфраструктуры. Бизнес-логика должна находиться в основном проекте приложения, где ее можно тестировать и модернизировать независимо от других обязанностей.
Доводя этот принцип в архитектуре приложения до логической завершенности, мы получим микрослужбы. Любая микрослужба должна иметь единственную обязанность. Если вам требуется расширить функциональные возможности системы, в большинстве случаев это рекомендуется делать путем добавления новых микрослужб, а не расширения обязанностей существующих.
Дополнительные сведения об архитектуре микрослужб
Принцип "Не повторяйся"
В приложении не следует определять поведение, связанное с конкретной концепцией, в нескольких расположениях, так как такой подход часто приводит к ошибкам. В какой-то момент в связи с изменением требований потребуется изменить такое поведение. В этом случае велик риск, что как минимум в одном расположении это обновление не будет проведено, и вся система станет несогласованной.
Вместо того чтобы дублировать логику, ее следует инкапсулировать в конструкции программирования. Такая конструкция должна быть единственным исполнителем нужного поведения и использоваться в любых других частях приложения в тех случаях, когда требуется реализовать это поведение.
Примечание.
Не рекомендуется связывать поведение, которое повторяется нерегулярно. Например, если две разных константы имеют одно значение, вместо них не нужно использовать одну константу, поскольку с точки зрения концепции они ссылаются на разные объекты. Дублирование всегда предпочтительнее взаимосвязи с неверной абстракцией.
Независимость сохраняемости
Принцип независимости сохраняемости относится к типам, для которых требуется сохранение состояния, однако код которых не зависит от выбираемой для этих целей технологии. В .NET такие типы иногда называются простыми объектами CLR (POCO), поскольку они не наследуются от конкретного базового класса и не реализуют определенный интерфейс. Принцип независимости сохраняемости очень важен, поскольку он позволяет сохранять состояние бизнес-модели различными способами, благодаря чему увеличивается гибкость приложения. Способы сохраняемости со временем могут изменяться. Например, вместо одной технологии базы данных может использоваться другая, а также могут потребоваться дополнительные способы (например, в дополнение к реляционной базе данных могут использоваться кэш Redis или Azure Cosmos DB).
Ниже приведены некоторые примеры нарушения этого принципа:
Обязательный базовый класс.
Обязательная реализация интерфейса.
Классы, отвечающие за сохранение самих себя (например, шаблон активной записи).
Требуется конструктор без параметров.
Свойства, использующие ключевое слово virtual.
Обязательные атрибуты сохраняемости.
Обязательное использование любых из указанных возможностей увеличивает степень связанности между типами, для которых требуется сохраняемость, и применяемой для этих целей технологией, что усложняет реализацию новых стратегий доступа к данным в будущем.
Ограниченные контексты
Принцип ограниченных контекстов является центральным при проблемно-ориентированном проектировании. Его соблюдение позволяет решить проблему сложности в крупных приложениях или организациях за счет разбиения на отдельные концептуальные модули. Каждый такой модуль представляет контекст, который отделен от других контекстов (то есть ограничен) и может развиваться независимо. В идеальном случае в каждом ограниченном контексте должны свободно выбираться имена используемых в нем концепций и обеспечиваться монопольный доступ к собственному хранилищу сохраняемости.
Как минимум, в отдельном веб-приложении необходимо постараться реализовать собственный ограниченный контекст со своим хранилищем сохраняемости для бизнес-модели вместо того, чтобы использовать общую для всех приложений базу данных. Взаимодействие между ограниченными контекстами осуществляется посредством программных интерфейсов, а не за счет общей базы данных, благодаря чему бизнес-логика и события могут реагировать на происходящие изменения. Ограниченные контексты тесно связаны с микрослужбами, которые в идеальном случае также реализуются в качестве отдельных ограниченных контекстов.