Часто ли вы слышите о новом принципе проектирования IT-архитектуры? А об обновлении классических принципов? Попробую вас удивить и привнести что-то новое.
Связанность и прочность (Coupling & Cohesion)
Обещал новое, а сам — за старое
Статья приурочена к 50-летию выхода первой статьи про Coupling & Cohesion — это случилось аж в декабре 1974: https://ieeexplore.ieee.org/document/5388187
Итак.
У вас никогда не вызывало недоумения, что связанность и прочность (или связность) — это про примерно одно и то же (и то, и другое — это некая связь), но одно — хорошо, а другое — почему-то плохо?
Но давайте по порядку.
В российских ИТ-публикациях используется несколько разных переводов на русский терминов Coupling и Cohesion. Приведу самые распространённые из них:
Coupling — зацепление, сцепление, связанность, сопряжение.
Cohesion — связность, прочность, сплоченность, сцепленность.
Чаще всего это противопоставление называют “связанность и связность” — но для меня это уж слишком ) Слишком легко запутаться ) В статье я буду использовать термины связанность (Coupling) и прочность (Cohesion).
Coupling
Связанность (зацепление, сцепление, сопряжение)

Связанность — это степень зависимости (использования) между различными компонентами. Компонентами могут выступать сущности совершенно разного масштаба — от методов и функций класса в ООП до больших систем и доменов внутри архитектуры предприятия. Для понимания, если прилично упростить, связанность между классами в ООП — это количество методов одного класса, который вызывает другой. Или в микросервисах — сколько других микросервисов дёргает по REST’у данный микросервис или на сколько очередей сообщений от других микросервисов он подписан. Хотя с очередями сложнее — там уровень зависимости ниже из-за асинхронности и отсутствия прямой связи между producer’ами и consumer’ами. Для простоты очереди пока отложим.
Cohesion
Прочность (связность, сплоченность, сцепленность)

Прочность — это степень зависимости между элементами внутри компонента. К примеру, использование друг другом методами одного класса в ООП. Или количество REST-вызовов внутри контекста микросервисов (для определения прочности этого самого ограниченного контекста микросервисов).
Как и связанность, прочность так же применяем для компонентов разного уровня — классам, модулям, микросервисам, контекстам микросервисов, большим системам и т.д.
На самом деле типов связанности и прочности, достаточно много, есть даже разные классификации типов (например, классификация связанности и классификация прочности), но сегодня мы не будем вдаваться в детали степени зависимости, а возьмем для простоты само наличие зависимости как меру связанности и прочности.
Мой любимый пример из википедии, демонстриурющий «хорошее и плохое» соотношение связанности и прочности:

-
“правильно” спроектированная архитектура компонента (класса, микросервиса и т.д.) — когда связанность низкая, а прочность высокая
-
и “неправильно” — с высокой связанностью и низкой прочностью.
Всё так, но ведь и связанность и прочность — это ведь просто некие связи между некими элементами. И эти связи иногда «хорошие», а иногда — «плохие». От чего же это зависит?
Что если мы возьмём картинки выше и попробуем «поиграть» с границами компонентов?


Получается, что связь становится характеристикой связанности или прочности в зависимости от проведения границ компонентов, к которым элементы связи принадлежат!

С проведением границ у нас всё хорошо — границы классов, микросервисов и т.д. всегда жёстко очерчены и воспринимаются всеми однозначно. Но вот количество границ — к сожалению или к счастью не ограничивается единицей ) Я имею в виду, что компоненты в большой сложной системе вложены друг в друга, и то, что являлось связанностью (скорее отрицательной характеристикой) на одном уровне — становится прочностью на следующем (т.к. становится внутренней связью компонента, а не внешней)!
Даже если взять микросервис, как единицу гранулярности — уровней компонентизации над ним может быть несколько:
-
ограниченный контекст микросервисов внутри продукта/системы
-
продукт/система
-
ландшафт предприятия (Enterprise архитектура)
А в больших продуктах и компаниях системы могут делиться на подсистемы или проекты, Enterprise уровень разделяется на архитектуру домена и междоменный ландшафт, а ещё добавляется слой или слои платформизации…

Сложность систем и компаний растёт, а стандартный приём борьбы со сложностью — это увеличение слоёв абстракции. Таким образом компонентизация архитектуры — это нормальный процесс, где количество уровней ничем не ограничивается.
При таком раскладе уже недостаточно стандартного разделения на архитектуру и архитекторов решений и предприятия (solution и enterprise), либо наоборот — грань этого разделения стирается.
Но вернёмся к связанности и прочности. Разберём на примерах.
На уровне микросервисной архитектуры продукта (или системы) у нас может быть несколько ограниченных контекстов. Рассмотрим пример из логистики, когда в периметре проекта несколько ограниченных контекстов (или контуров), отвечающих за пункты выдачи заказов (ПВЗ), географию и службы доставки. На схеме представлена микросервисная архитектура контекста с ПВЗ и её связь с другими контекстами:

У контекстов микросервисов есть внутренняя прочность — например, взаимосвязи микросервисов внутри контекста ПВЗ.
И внешняя связанность — взаимосвязи между контекстами внутри продукта “Логистика”:

Но если мы пойдем на уровень выше — на уровень взаимодействия между продуктами, где логистика взаимодействует с витриной и другими системами:

Окажется, что связанность контекстов микросервисов стала прочностью продукта “Логистика”, связанность которого в свою очередь уже определяется его зависимостями от других продуктов. То есть на нашем примере запросы географии и службы доставки — это и мера связанности на уровне архитектуры продукта “Логистика”, и мера уже прочности на межпродуктовом (enterprise) уровне архитектуры.

Напомню, что низкая связанность — хорошо, а низкая прочность — плохо. Так как быть, если одно переходит в другое при изменении уровня компонентизации, на котором рассматриваем архитектуру?
Логичным выводом будет переход от бинарного деления на связанность и прочность к цепочке зависимостей:
Микросервисы → Контексты микросервисов → Продукты → Архитектура предприятия
Как писал выше, звеньев в такой цепочке на самом деле может быть сколько угодно. И для таких цепочек сформулируем новое правило взамен низкой связанности и высокой прочности:
Мера зависимостей между элементами каждого последующего уровня должна быть не выше, чем у предыдущего
То есть, если упростить меру до количества REST-вызовов — то количество вызовов между микросервисами внутри контекстов должно быть не меньше, чем между контекстами, которое в свою очередь не меньше количества вызовов между продуктами. И так далее. И это касается любой компонентной архитектуры, не обязательно микросервисной.
В реальности при подсчёте меры зависимости необходимо учитывать ещё и силу связи (к примеру, у ассинхронных связей она будет ниже чем у синхронных. Но об этом поговорим в следующих статьях). В примере ниже для простоты будем оперировать только синхронными связями.

У Роберта Мартина среди компонентных принципов есть SAP (Stable Abstraction Principle), который говорит о том что абстрактность компонента должна нарастать с его стабильностью.
Сформулируем новый принцип для уровней абстракции элементов архитектуры — связанность компонентов уровня не должна нарастать с повышением абстракции уровня.
Под уровнем имеется ввиду именно уровень компонентизации элементов архитектуры (микросервис, контекст, продукт/система, домен и т.д.), нельзя путать со слоями (например, слой доступа к данным или платформенный слой) внутри архитектурного уровня.
Пример расчета

Возьмём пример простой микросервисной архитектуры, на котором будем практиковаться.
Давайте считать
У контекста 1 внутренняя прочность — 3 условные единицы (пусть REST-вызова), внешняя связанность — 2.
У контекста 2 внутрення прочность — 1, внешняя связанность — 0.
У нашего проекта внутренняя прочность — 2 (связи между контекстами), внешняя связанность — 1.
Правило не нарушается
Тут важно заметить, что у контекста 2 связанность равна нулю, так как он ни от чего не зависит. То есть нужно учитывать не только наличие связей, но и их направление (в данном примере направление стрелочек означает направление REST-запросов).
Можно посчитать и чуть иначе — перейти к неравенству, описывающему цепочку уровней компонентизации. В таком случае неравенств будет несколько — по одному на каждый элемент нижнего уровня компонентизации:
Начинаем считать с Контекста 1. У него внутренняя прочность — 3, внешняя связанность — 2. У нашего проекта внутренняя прочность — 2, внешняя связанность — 1.
3 ≥ 2 ≥ 1
Если считать с Контекста 2. У него внутренняя прочность — 1, внешняя связанность — 0. И у нашего проекта относительно контекста 2 внешняя связанность — 0.
1 ≥ 0 ≥ 0 Тут важно, что у Контекста 2 нет внешних связей ни на уровне проекта, ни на уровне выше — поэтому, начиная считать с него, относительно Контекста 2 у проекта также нет внешней связанности.
Ещё одно следствие из принципа — прочность компонента должна быть меньше суммы прочностей компонентов этого компонента.
Как можно автоматизировать и отслеживать соблюдение принципа?
Какие в целом существуют подходы работы с организацией и разделением уровней доступа к API распределенных систем? Конечно, есть паттерн API Gateway. Но проблема в том, что в классической реализации он позволяет организовать взаимодействие между максимум двумя уровнями компонентизации архитектуры. То есть либо мы организуем плоскую структуру с заведением в API Gateway всех наших микросервисов (и всех их API):

Либо договариваемся, что, к примеру, внутри продукта/системы микросервисы ходят друг к другу напрямую, а между системами — через API Gateway:

В таком виде эффективно не удастся организовать и отслеживать зависимости и взаимодействия на нескольких уровнях — например, между микросервисами, между контекстами и между продуктами/системами.

Можно конечно развернуть несколько API Gateway’ев для разных уровней и соединить их каскадом, но, на мой взгляд, это лишь добавит сложности. А любые принципы проектирования и подходы к реализации должны наоборот со сложностью бороться.
Так что же делать?
Я применяю и всячески пропагандирую подходы Everything as Code. Конкретно в данном случае нас будет интересовать “Architecture as Code” (AaC) и/или “Infrastructure as Code” (IaC). Если мы сможем получать актуальный граф зависимостей между компонентами нашей системы — то мы сможем написать обычный unit-тест, который будет проверять выполняется ли принцип!
Тест на архитектуру можем написать, например, в таком виде:
// для всех наших контекстов и периметров for (const boundary of pumlFile.boundaries) { const cohesion = GetBoundaryCohesion(boundary); const coupling = GetBoundaryCoupling(boundary); // во-первых, внутренняя прочность периметра должна быть больше внешней связанности expect(cohesion).toBeGreaterThan(coupling); // во-вторых, если периметр содержит в себе другие периметры — его прочность должна быть меньше суммы прочностей внутренних периметров if (boundary.boundaries.length > 0) expect(cohesion).toBeLessThan(<сумма прочностей внутренних контекстов>); } });
Приведенный выше пример архитектуры (в формате AaC) и реализация простого теста на проверку принципа выложена в OpenSource-репозитории с инструментами для покрытия архитектуры as code тестами.
Что ещё за покрытие архитектуры тестами и репозиторий?!:)
Об этом я писал ранее в статье: https://habr.com/ru/articles/800 205/
В ней же описан подход автоматической проверки, что наша архитектура (граф зависимостей) актуальна и соответствует реальным зависимостям на проде; а также приведены примеры тестов на различные архитектурные принципы.Все инструменты, примеры тестов и инфраструктура для работы с AaC (и для архитектуры модульных монолитов!) разрабатываем в OpenSource‑репозитории: https://github.com/Byndyusoft/aact
Присоединятесь к контрибуции
Что делать, если принцип нарушен
Нарушение принципов проектирования как правило идёт в связке, сложно придумать пример, где нарушается ровно один принцип при соблюдении остальных. Различные принципы проектирования призваны скорее с разных сторон описать (и по возможности замерить) качество архитектуры и подсветить проблемы.
Нарушение принципа каскадного снижения связанности, на мой взгляд, чаще всего идёт в совокупности с проблемами ещё с двумя принципами компонентизации, описанными Робертом Мартином:
-
Stable Dependencies Principle: Depend in the direction of stability
-
Stable Abstractions Principle: A component should be as abstract as it is stable

О том, как отрефакторить микросервисную архитектуру при нарушении этих и других принципов проектирования, я рассказывал в своём докладе: https://rutube.ru/video/022a92df83d7737a7fcad3d1a67ebc9b/
Итак, Принцип каскадного снижения связанности
Прошу любить и жаловать
В случае сложных многоуровневых систем важно отслеживать падение (ненарастание) связанности элементов при увеличении уровня рассмотрения архитектуры. И наоборот — при погружении в более детальные системы и контексты прочность внутренних элементов должна нарастать (или хотя бы оставаться той же).

Принцип каскадного снижения связанности является логичным развитием концепций связанности и прочности для сложных систем, рассматриваемых на разных уровнях. Принцип определяет правило построения зависимостей на Enterprise и Solution уровнях, а также между ними. Также принцип позволяет разделить ИТ-архитектуру на произвольное количество уровней рассмотрения или периметров группировки элементов и говорит о хороших практиках распределения связей между ними.

Руслан Сафин
ИТ‑архитектор, эксперт по распределённым системам
Развиваю и продвигаю новые подходы в ИТ в России на уровне отрасли
Свои изыскания в сфере распределённых систем в виде мыслей статей и opensource-инструментов публикую в своём канале: Архитектура распределённых систем
ссылка на оригинал статьи https://habr.com/ru/articles/894766/
Добавить комментарий