open atlas
↑ К треку
Архитектурные паттерны ARCH · 02 · 03

Ациклические и стабильные абстракции

ADP, SDP и SAP расширяют правило зависимостей на весь граф. ADP: никаких циклов. SDP: зависи в сторону стабильности. SAP: стабильные — абстрактные. Определяют main sequence и зоны Pain и Uselessness.

ARCH Middle ◷ 24 min
Уровень
ОсновыJuniorMiddleSenior

Через шесть месяцев после наведения порядка с DIP на B2B-платформе команда запустила анализ графа зависимостей и обнаружила кое-что неожиданное. Компонент billing и компонент order были чисто разделены — прямых импортов между ними не было. Но граф показал цикл: billing импортировал компонент shared-utils (для форматирования денег), shared-utils импортировал компонент order (для enum OrderStatus, который постепенно в него переполз), а order импортировал billing (для валидации ссылок на инвойсы). Ни один из трёх компонентов не импортировал другие напрямую. Цикл шёл через разделяемую библиотеку, которой никто не владел. Каждый раз, когда кто-то в цикле что-то менял — даже хелпер форматирования — все три компонента нужно было выкатывать вместе. Команда достигла DIP на уровне двусторонних отношений и всё равно построила распределённый big ball of mud на уровне графа компонентов. Три принципа этого урока работают на том более высоком уровне: не направление одной стрелки зависимости, а форма всего графа зависимостей.

ADP: Acyclic Dependencies Principle

ADP формулируется просто: в графе зависимостей компонентов не должно быть циклов. Цикл — набор компонентов, где каждый в конечном счёте зависит от остальных — напрямую или транзитивно. Вводная история — классический пример: billing → shared-utils → order → billing.

Циклы разрушительны по трём причинам:

Они превращают независимые компоненты в одну единицу выпуска. Если A зависит от B зависит от C зависит от A, нельзя собрать или развернуть ни один из них без остальных. Их нужно выпускать группой, даже если они спроектированы как отдельные компоненты.

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

Они вынуждают более высокие метрики coupling. Каждый компонент в цикле фактически имеет зависимость в исходном коде от внутренностей каждого другого. Метрики afferent/efferent coupling становятся бессмысленными.

Разрыв циклов: два способа

При обнаружении цикла есть два структурных инструмента:

1. Применить DIP для инверсии одной из стрелок. Если A → B → C → A — цикл, введи интерфейс в A, который реализует C. Стрелка C → A становится C → IA (где IA живёт в пакете A), и A импортирует только IA. Цикл разорван.

2. Извлечь новый компонент, от которого могут зависеть обе стороны. Если A и C зависят друг от друга потому что разделяют что-то (enum OrderStatus из вводной истории), извлеки это в новый компонент D без зависимостей. Тогда A → D, C → D. Оба зависят от D; ни один не зависит от другого.

В вводной истории решением был вариант 2: извлечь enum OrderStatus из shared-utils в новый компонент order-domain-types без зависимостей. Billing импортирует order-domain-types напрямую. Order импортирует order-domain-types. shared-utils больше не импортирует order. Цикл исчез.

SDP: Stable Dependencies Principle

SDP расширяет правило зависимостей до уровня компонентов: зависи только в сторону стабильности. Компонент должен зависеть только от компонентов, которые по меньшей мере так же стабильны, как он сам.

Стабильность на уровне компонентов измеряется метрикой нестабильности из unit 01:

I = Ce / (Ca + Ce)

где Ce — количество исходящих зависимостей (efferent), Ca — количество входящих (afferent). I = 0 — максимально стабильный. I = 1 — максимально нестабильный.

SDP гласит: если компонент A с нестабильностью I_A зависит от компонента B с нестабильностью I_B, должно выполняться I_A ≥ I_B. Стрелка должна указывать от более высокой нестабильности к более низкой — от более volatile к более стабильному.

Сценарий нарушения SDP: volatile компонент, от которого зависит стабильный компонент. Стабильный компонент был спроектирован с трудом поддающимся изменениям. Но теперь у него есть зависимость на что-то volatile. Volatile компонент меняется — и вынуждает меняться стабильный, несмотря на его высокий afferent coupling.

На B2B-платформе billing engine (Ca = 12, I ≈ 0.08 — очень стабильный) должен зависеть только от компонентов со схожей низкой нестабильностью: интерфейс IOrderPort (I = 0 — чистая абстракция), утилита money (I = 0.1 — стабильная). Он не должен зависеть от компонента reporting utilities (I = 0.9 — почти ничто от него не зависит, сам зависит от многого, меняется еженедельно). Если billing импортирует reporting utilities, еженедельное изменение отчётности теперь вынуждает проверять billing.

Почему это работает

Почему нарушение SDP так неприятно на практике? Потому что удивляет. Модуль с I ≈ 0 должен быть фундаментом — тем, что никогда не меняется и от чего все могут зависеть. Когда он ломается во вторник из-за того, что коллега поменял хелпер логирования, который фундаментный модуль никогда не должен был импортировать, нарушение ментальной модели оглушает. SDP делает эту ментальную модель структурной: стрелки зависимостей кодируют иерархию стабильности.

SAP: Stable Abstractions Principle

SAP — партнёр SDP: компонент должен быть настолько абстрактным, насколько он стабилен. Стабильность и абстрактность должны масштабироваться вместе.

Это не эстетическое предпочтение — оно следует из правила зависимостей. Если компонент стабильный (высокий Ca, низкий I), многие вещи от него зависят. Эти зависимые не могут легко его изменить при эволюции требований. Единственный способ, которым стабильный компонент может оставаться полезным при изменении требований без вынуждения изменений у всех зависимых — быть абстрактным: определённым как интерфейсы и абстрактные типы, которые можно расширять без модификации. Стабильный конкретный компонент нельзя расширить без его изменения — а изменение ломает всех зависимых.

Две зоны отказов следуют напрямую:

Zone of Pain (стабильный + конкретный, I ≈ 0, A ≈ 0): компонент с высокой стабильностью (много зависимых, сложно менять), но полностью конкретный (всё реализация, никаких интерфейсов). Если этот компонент когда-либо должен измениться, каждый зависимый должен измениться тоже. Конкретный инфраструктурный код, который импортирует каждый модуль, но который обновляется с каждым апгрейдом фреймворка — живёт здесь.

Zone of Uselessness (нестабильный + абстрактный, I ≈ 1, A ≈ 1): компонент полностью абстрактный (только интерфейсы), но никто от него не зависит. Эти интерфейсы, вероятно, были написаны для будущего кейса, который так и не пришёл. Они бесполезны: никакой конкретный компонент не реализует их, никакой компонент не зависит от них.

Main sequence — диагональная линия между двумя зонами: идеальное положение компонента — где его абстрактность примерно равна его нестабильности (A ≈ I). Максимально стабильный компонент (I = 0) должен быть максимально абстрактным (A = 1) — чистый пакет интерфейсов. Максимально нестабильный (I = 1) должен быть максимально конкретным (A = 0) — листовая реализация.

Применение трёх принципов вместе

ADP, SDP и SAP наиболее эффективны при совместном применении как полный набор ограничений для графа компонентов:

  1. Сначала ADP: обнаружь и разорви циклы. Цикличный граф делает метрики нестабильности (Ca/Ce) некорректными.

  2. Вычисли метрики нестабильности после очистки ADP: с ациклическим графом измерь I для каждого компонента.

  3. Применяй SDP: проверь, что каждая стрелка зависимости указывает от более высокого I к более низкому. Зависимости, нарушающие это — первые кандидаты на инверсию через DIP.

  4. Применяй SAP: нанеси компоненты на плоскость I/A. Компоненты далеко от main sequence — структурный долг: zone-of-pain отклонения нуждаются в извлечении интерфейсов; zone-of-uselessness отклонения нужно либо реализовать, либо удалить.

На B2B-платформе после исправления цикла оставшийся структурный долг был компонент shared-utils в zone of pain: I ≈ 0.05 (многие зависят от него), но A = 0 (чисто конкретные утилитарные функции). Исправление: извлечь интерфейсы, которые реально нужны вызывающим, в небольшой чисто-абстрактный пакет platform-contracts, а конкретные утилиты понизить до компонента, от которого зависят только листовые модули.

lesson.inset.note

Main sequence — цель, а не требование. Некоторые компоненты законно стоят вне диагонали — чисто конфигурационный класс без зависимых и без абстракций (I ≈ 1, A = 0) нормален, потому что не имеет структурного влияния. Zones of Pain и Uselessness — предупреждения, не запреты. Используй I/A-график для выявления наибольших отклонений и исправляй их первыми.

Викторина

Граф зависимостей показывает: OrderModule → BillingModule → SharedUtils → OrderModule. Команда хочет разорвать цикл. Какие два структурных способа доступны и когда предпочтителен каждый?

Викторина

Компонент billing engine имеет Ca = 15 (15 компонентов зависят от него) и Ce = 1 (зависит только от чисто-абстрактного интерфейса IPaymentPort). По SAP, что должно быть верно о компоненте billing engine и удовлетворяет ли описанная структура этому?

Викторина

Компонент reporting имеет I = 0.95 (почти ничто не зависит от него; он зависит от многого) и A = 0.9 (почти полностью интерфейсы, без конкретной логики). Где он находится на плоскости I/A и является ли это структурной проблемой?

Вспомните перед уходом
  1. 01
    Почему ADP нужно применять до вычисления метрик SDP и SAP?
  2. 02
    Что такое Zone of Pain и Zone of Uselessness и какой структурный ход исправляет каждую?
  3. 03
    Что представляет main sequence и как использовать I/A-график для приоритизации архитектурной работы?
Итог

ADP, SDP и SAP — три принципа на уровне компонентов, расширяющие правило зависимостей до уровня всей кодовой базы.

Acyclic Dependencies Principle гласит, что граф зависимостей компонентов должен быть направленным ациклическим графом (DAG). Циклы вынуждают компоненты в совместные единицы выпуска, раздувают зону взрыва изменений и искажают метрики стабильности. Разрывай циклы через DIP (инвертируй одну стрелку интерфейсом) или извлечение (выноси разделяемые элементы в новый листовой компонент без зависимостей).

Stable Dependencies Principle гласит, что стрелки зависимостей должны указывать от более высокой нестабильности к более низкой. Нестабильность: I = Ce/(Ca+Ce); нуль — максимально стабильный, единица — максимально volatile. Стабильный компонент, зависящий от volatile — нарушение SDP: фундамент архитектуры может быть сломан изменением в чём-то, предназначенном для замены. Исправляй нарушения SDP применением DIP на нарушающем ребре.

Stable Abstractions Principle гласит, что абстрактность должна масштабироваться со стабильностью. Стабильный компонент (I ≈ 0), который при этом конкретный (A ≈ 0), находится в Zone of Pain: сложно менять, невозможно расширить без поломки зависимых. Нестабильный компонент (I ≈ 1), полностью абстрактный (A ≈ 1), находится в Zone of Uselessness: все контракты, никакой реализации. Main sequence (A ≈ I) — цель. Расстояние от main sequence количественно выражает архитектурный долг и приоритизирует структурную работу по рефакторингу.

Применённые вместе — сначала ADP, затем SDP и SAP — эти три принципа дают полный измеримый словарь для оценки и улучшения формы всего графа зависимостей компонентов.

Практика

Начни сверху. Задачи идут от простого к сложному: вспомнить факт, применить к случаю, затем senior-уровень. Открой, попробуй, потом открой ответ.

вспомнитьприменитьуглубить0 из 3 завершено

Что-то непонятно?

Задай вопрос по этому уроку. Вопросы анонимны и попадают напрямую автору — урок станет лучше.

хоткеи развернуть
поиск
K
пред. пьеса
k
след. пьеса
j
тиры
t
это меню
?
sources3
expand
  1. 01
  2. 02
  3. 03

Trademarks belong to their respective owners. Editorial reference only.