Связанность, связность и закон Деметры
Если из всей теории проектирования вам разрешат унести с собой ровно две идеи — берите эти две. Низкая связанность (coupling) между модулями и высокая связность (cohesion) внутри модуля. Почти всё остальное — SOLID, паттерны, гексагональная архитектура, микросервисы — это способы добиться этих двух свойств в конкретных обстоятельствах. Не наоборот.
Мы уже разбирали пять принципов SOLID в статье SOLID: пять принципов с честным разбором и критикой и цену преждевременных абстракций в DRY, KISS, YAGNI. Здесь — фундамент, на котором они стоят, причём фундамент этот старше их лет на пятнадцать.
1. Интуиция: почему это вообще важно
Представьте, что вам надо поменять формат телефонного номера. Задача на десять минут. В одной кодовой базе вы правите один файл, прогоняете тесты, мержите. В другой — grep находит 47 мест, три из них в чужих сервисах, одно в SQL-миграции, ещё одно в отчёте, про который никто не помнит. Код одинаково «работает» в обоих случаях. Разница — исключительно в структуре связей.
Формально: связанность — это мера того, насколько изменение в одном модуле вынуждает менять другой. Связность — мера того, насколько элементы внутри модуля принадлежат друг другу и служат одной цели.
Ключевая мысль, которую часто упускают: это не два независимых регулятора. Они почти всегда движутся вместе. Модуль с низкой связностью (свалка разнородных функций) обязательно окажется сильно связан со всеми — потому что каждый его пользователь тянет за собой весь ворох чужих зависимостей. И наоборот: когда вы честно собираете в модуле всё, что меняется по одной причине, границы модуля становятся узкими сами собой.
«Связность и связанность — не два принципа, а один принцип, увиденный с двух сторон.» Идея восходит к Ларри Константину: Structured Design (Constantine, Yourdon, 1979) — книга, где обе шкалы впервые описаны формально.
Единица измерения тут не эстетическая, а экономическая: сколько файлов приходится открыть, чтобы внести одно осмысленное изменение. Эта величина имеет имя — change amplification (усиление изменения), её вводит Джон Оустерхаут в A Philosophy of Software Design.
2. Шкала связанности и шкала связности
Константин и Йордон предложили не бинарное «хорошо/плохо», а упорядоченные шкалы. Это важнее, чем кажется: они позволяют говорить «здесь у нас control coupling, давайте опустим до data coupling» вместо бесполезного «код связный какой-то».
2.1 Связанность, снизу вверх (от худшего к лучшему)
Content coupling (контентная, патологическая). Модуль А лезет во внутренности
модуля Б: правит его приватные поля, прыгает в середину его кода, полагается на
представление его данных. В Python — обращение к obj._internal_cache. В C — прыжок
по указателю в чужой буфер. Любое изменение Б ломает А, причём молча.
Common coupling (общая). Модули общаются через общее изменяемое состояние: глобалы, синглтон-конфиг, мутируемый модуль-уровень словарь. Смертельность в том, что связь не видна в сигнатурах — граф зависимостей есть, но его нельзя ни прочитать, ни проверить линтером.
# Common coupling: связь не видна из сигнатур
CURRENT_TENANT = {} # глобальное изменяемое состояние
def load_request(req):
CURRENT_TENANT["id"] = req.headers["X-Tenant"]
def build_invoice(order):
# нигде в сигнатуре не сказано, что нужен CURRENT_TENANT
return Invoice(order, tenant=CURRENT_TENANT["id"])
Тест build_invoice не запустится без load_request, а это в тесте выглядит как
загадочный KeyError. Лечение — сделать зависимость явной: build_invoice(order, tenant_id).
External coupling (внешняя). Модули завязаны на общий внешний контракт: формат CSV-файла, схему таблицы, протокол. Полностью убрать нельзя (система должна с чем-то разговаривать), но нужно локализовать: один модуль знает про формат, остальные — про доменный тип.
Control coupling (управляющая). Вызывающий передаёт флаг, управляющий внутренней логикой вызываемого. Классика:
# Плохо: control coupling — вызывающий знает про ветки внутри
def export(report, as_pdf: bool, compress: bool, notify: bool): ...
export(report, True, False, True) # что тут происходит, читать невозможно
Проблема не в «булевых аргументах» как таковых (это следствие), а в том, что вызывающий обязан знать внутреннее устройство вызываемого. Добавили ветку — поменяли сигнатуру у всех.
# Лучше: разные операции — разные функции, ветвление уехало наверх
def export_pdf(report) -> bytes: ...
def export_csv(report) -> bytes: ...
# либо: стратегия как параметр — вызывающий передаёт «что», а не «как»
def export(report, fmt: ReportFormat) -> bytes:
return fmt.render(report)
Stamp coupling (структурная). Передаём целую структуру, а нужны два поля. Модуль
формально зависит от всего типа: поменяли User — пересобирается и перетестируется
функция, которой нужен был только user.email.
# Stamp: зависимость от всего User ради одного поля
def send_welcome(user: User) -> None:
mailer.send(user.email, template="welcome")
# Data: зависимость ровно от того, что используется
def send_welcome(email: str) -> None:
mailer.send(email, template="welcome")
Оговорка от честности: доводить до абсурда не надо. Если функции нужны семь полей
User, семь параметров — хуже, чем один объект. Правило простое: передавай
структуру, если модуль концептуально работает с сущностью; передавай поля, если он
работает с двумя значениями.
Data coupling (данные). Обмен только теми данными, что реально нужны, через явные параметры и возвраты. Это цель.
Некоторые авторы добавляют сверху message coupling — взаимодействие только через сообщения/события, без общих типов вовсе. Это то, к чему стремятся event-driven системы, и заодно источник их собственной боли: связанность уходит из компилятора в рантайм, где её никто не проверяет.
2.2 Связность, сверху вниз (от лучшего к худшему)
Functional — модуль делает ровно одну вещь: parse_iso8601. Sequential — выход
одной части служит входом следующей (пайплайн разбора). Communicational — части
работают над одними данными (репозиторий заказа). Все три — здоровые.
Procedural — объединяет только порядок вызова. Temporal — «всё, что делается
при старте»: типичный bootstrap.py, где рядом живут миграции БД, прогрев кэша и
регистрация метрик. Терпимо для инфраструктурного клея, вредно для домена.
Logical — «все валидации», «все хендлеры», где нужная ветка выбирается по флагу.
Coincidental — utils.py, helpers.py, common/: сюда кладут то, чему не нашли
места. Это худшая связность, и её симптом узнаётся мгновенно: имя модуля нельзя
сузить — про что utils? Про всё, значит ни про что.
Практический тест на связность формулируется как вопрос: «Какое изменение требований заставит меня открыть этот файл?» Если ответов один-два — связность высокая. Если десяток разнородных — модуль надо резать. Это, по сути, формулировка Принципа единственной ответственности (SRP) в терминах связности; подробнее — в статье про SOLID.
3. Коннасценция: более точный язык
Шкалы Константина хороши, но грубоваты: они говорят «это control coupling», но не говорят, насколько это больно и что делать. Мейлир Пейдж-Джонс в What Every Programmer Should Know About Object-Oriented Design (1996) предложил более тонкую модель — коннасценцию (connascence, «со-рождённость»). Два фрагмента кода коннасцентны, если изменение одного требует изменения другого, чтобы система осталась корректной.
Модель даёт три оси оценки — сила, степень и локальность, — и это её главная ценность: сравнивать можно не только «что», но и «насколько плохо».
Три правила работы с коннасценцией (сам Пейдж-Джонс формулирует их как рекомендации):
- Минимизируй общую коннасценцию, разбивая систему на инкапсулированные части.
- Минимизируй коннасценцию, пересекающую границы: внутри класса связь имени — бесплатна, между сервисами — катастрофа.
- Максимизируй коннасценцию внутри границ — это и есть высокая связность.
Ключевой практический вывод: связь тем допустимее, чем она локальнее. Коннасценция позиции (порядок аргументов) внутри приватного метода на две строки — вообще не проблема. Она же между двумя сервисами разных команд (порядок полей в бинарном формате) — инцидент, ждущий своего часа.
# Коннасценция позиции: вызывающий обязан помнить порядок
create_user("Ivan", "Petrov", "ivan@example.com", True, False)
# Понижена до коннасценции имени — компилятор/линтер поможет, порядок не важен
create_user(
first_name="Ivan",
last_name="Petrov",
email="ivan@example.com",
is_active=True,
is_admin=False,
)
Это типичный рефакторинг «понижение коннасценции»: позиция → имя. Другие частые: значение → имя (магическое число → константа/енум), алгоритм → имя (два дублирующих вычисления → одна общая функция).
4. Афферентная и эфферентная связанность: измеряем
Слово «связанный» бесполезно, пока его нельзя посчитать. Роберт Мартин в Agile Software Development: Principles, Patterns, and Practices ввёл метрики уровня компонента (оригинальная заметка — «OO Design Quality Metrics», 1994):
- Ca (afferent coupling) — сколько внешних классов зависят от компонента. Мера ответственности: чем больше, тем дороже его менять.
- Ce (efferent coupling) — от скольких внешних классов зависит сам компонент. Мера уязвимости: чем больше, тем чаще его ломают чужие изменения.
- I = Ce / (Ca + Ce) — нестабильность, от 0 (максимально стабильный: все зависят от него, он ни от кого) до 1 (максимально нестабильный).
- A = (абстрактные типы) / (все типы) — абстрактность компонента.
- D = |A + I − 1| — расстояние до «главной последовательности», прямой A + I = 1.
Смысл главной последовательности прост и красив: стабильное должно быть абстрактным, изменчивое — конкретным. Компонент, от которого зависит полсистемы, обязан быть набором интерфейсов и правил, иначе его нельзя менять. Компонент, от которого не зависит никто, может быть сколь угодно конкретным.
Две патологии по углам:
- Зона боли (стабильный + конкретный): от него все зависят, а он — жёсткий код без
абстракций. Классический жилец — тот самый
utils/common, куда все импортируются. Менять его страшно, а не менять нельзя. - Зона бесполезности (нестабильный + абстрактный): гора интерфейсов, которыми никто не пользуется. Обычно это следы архитектуры «на вырост», о цене которой — в статье про YAGNI.
4.1 Считаем метрики сами
Готовые инструменты есть (о них ниже), но полезно понимать, что внутри — это просто обход графа импортов.
"""Расчёт Ca/Ce/I по графу зависимостей пакетов."""
from collections import defaultdict
from dataclasses import dataclass
# граф: пакет -> пакеты, которые он импортирует (эфферентные рёбра)
Graph = dict[str, set[str]]
@dataclass(frozen=True)
class Metrics:
ca: int # входящие зависимости
ce: int # исходящие зависимости
instability: float
def compute(graph: Graph) -> dict[str, Metrics]:
"""O(V + E) по времени и O(V + E) по памяти: один проход по всем рёбрам."""
incoming: dict[str, set[str]] = defaultdict(set)
for src, targets in graph.items():
for dst in targets:
if dst != src: # самозависимости не считаем
incoming[dst].add(src)
result: dict[str, Metrics] = {}
for pkg in graph:
ca = len(incoming[pkg])
ce = len({d for d in graph[pkg] if d != pkg})
total = ca + ce
# соглашение: изолированный пакет считаем максимально стабильным
instability = (ce / total) if total else 0.0
result[pkg] = Metrics(ca=ca, ce=ce, instability=round(instability, 2))
return result
def find_cycles(graph: Graph) -> list[list[str]]:
"""Поиск циклов зависимостей обходом в глубину с тремя цветами. O(V + E)."""
WHITE, GREY, BLACK = 0, 1, 2
color: dict[str, int] = defaultdict(int)
stack: list[str] = []
cycles: list[list[str]] = []
def dfs(node: str) -> None:
color[node] = GREY
stack.append(node)
for nxt in graph.get(node, ()):
if color[nxt] == GREY: # нашли обратное ребро
cycles.append(stack[stack.index(nxt):] + [nxt])
elif color[nxt] == WHITE:
dfs(nxt)
stack.pop()
color[node] = BLACK
for node in graph:
if color[node] == WHITE:
dfs(node)
return cycles
if __name__ == "__main__":
g: Graph = {
"domain": set(),
"ports": {"domain"},
"postgres": {"ports", "domain"},
"http": {"ports", "domain"},
"app": {"http", "postgres", "ports"},
}
for pkg, m in sorted(compute(g).items(), key=lambda kv: kv[1].instability):
print(f"{pkg:10s} Ca={m.ca} Ce={m.ce} I={m.instability}")
print("циклы:", find_cycles(g) or "нет")
Вывод показывает ровно то, чего ждёшь от здоровой слоистой системы: domain с I = 0,
app с I = 1, адаптеры посередине. Отдельно стоит find_cycles: цикл зависимостей
между пакетами — это не метрика, а дефект. Мартин формулирует это как ADP (Acyclic
Dependencies Principle): граф зависимостей компонентов обязан быть ациклическим,
иначе «компоненты» — фикция, они собираются, релизятся и ломаются только вместе.
Сложность обеих функций — O(V + E) по времени, O(V + E) по памяти. На реальном монорепо с десятками тысяч модулей это доли секунды, так что проверку спокойно вешают в CI.
5. Закон Деметры
Закон Деметры (Law of Demeter, LoD) — самое операциональное правило снижения связанности из существующих: его можно проверить глазами за секунду. Сформулирован в Northeastern University в 1987 году (Lieberherr, Holland, «Assuring Good Style for Object-Oriented Programs»), в проекте с кодовым именем Demeter — отсюда название.
Формулировка для методов: метод M объекта O может вызывать только методы:
- самого объекта O (включая унаследованные);
- объектов, переданных M как аргументы;
- объектов, которые M создал сам;
- объектов, являющихся полями (компонентами) O.
Ключевой запрет: нельзя вызывать методы объектов, полученных из других вызовов. Народная формулировка — «не разговаривай с незнакомцами» или «используй только одну точку» (последняя неточна, но помогает запомнить).
5.1 Поезд вагонов
# «Train wreck»: метод знает про Order, Customer, Address и City
def shipping_label(order: Order) -> str:
return order.get_customer().get_address().get_city().upper()
Этот метод объявил зависимость от одного типа, а на деле зависит от четырёх и от
структуры связей между ними. Уберите Address в пользу Location — сломается код,
который про адреса вообще не думал. Приблизительно оценить масштаб можно так: цепочка
длины k даёт зависимость примерно от k типов вместо одного, и вероятность поломки
растёт с ростом k мультипликативно.
# Вариант 1: делегирование — Order отвечает на вопрос сам
class Order:
def shipping_city(self) -> str:
return self._customer.shipping_city()
def shipping_label(order: Order) -> str:
return order.shipping_city().upper()
# Вариант 2: попросить ровно то, что нужно (data coupling)
def shipping_label(city: str) -> str:
return city.upper()
Второй вариант почти всегда лучше первого: он не только соблюдает LoD, но и убирает
зависимость от Order целиком. Первый вариант — вынужденный, когда логика реально
принадлежит заказу.
5.2 Tell, Don’t Ask
Закон Деметры — частный случай более общего принципа «говори, не спрашивай» (Tell Don’t Ask, martinfowler.com): не вытаскивай данные из объекта, чтобы принять решение снаружи, — попроси объект принять решение самому.
После рефакторинга остаётся одна стрелка: BillingService ..> Order, а Order сам
спрашивает своего Customer. Знание о том, что скидка зависит от уровня членства,
переезжает туда, где ему место, — вместе с данными.
# ДО: сервис вытаскивает данные и решает за домен
class BillingService:
def charge(self, order: Order) -> Money:
tier = order.get_customer().get_membership().get_tier()
rate = {"gold": 0.15, "silver": 0.07}.get(tier, 0.0)
return order.get_total() * (1 - rate)
# ПОСЛЕ: домен решает сам, сервис только оркестрирует
class Membership:
def discount_rate(self) -> float:
return {"gold": 0.15, "silver": 0.07}.get(self._tier, 0.0)
class Customer:
def discount_rate(self) -> float:
return self._membership.discount_rate()
class Order:
def payable_amount(self) -> Money:
return self._total * (1 - self._customer.discount_rate())
class BillingService:
def charge(self, order: Order) -> Money:
return order.payable_amount()
Обратите внимание: правило скидок теперь лежит рядом с данными о членстве. Это и есть рост связности — тот самый второй регулятор, который двигается вместе с первым.
5.3 Когда закон Деметры не применяется
Здесь важна честность, иначе принцип превращается в карго-культ (о чём — в обзоре трека).
LoD — про объекты с поведением, а не про структуры данных. У dict, DTO, protobuf-
сообщения, JSON-ответа нет поведения, которое можно инкапсулировать; config.db.pool.size
— не нарушение, это просто навигация по данным. Оустерхаут (A Philosophy of Software
Design) прямо критикует буквальное применение LoD: обёртки ради обёрток порождают
«мелкие модули», где интерфейс дороже реализации.
Fluent-интерфейсы и цепочки — не нарушение. query.where(...).order_by(...).limit(10)
и stream.filter(...).map(...).collect(...) возвращают тот же (или родственный) тип
и не раскрывают внутреннюю структуру. Правило «одна точка» здесь просто не про то.
Главная цена LoD — разрастание интерфейсов. Соблюдая букву закона, вы добавляете
делегирующие методы: Order.shipping_city(), Order.customer_email(),
Order.customer_locale(). Это реальный риск, у него даже есть имя — Demeter
transmogrifier. Поэтому применяйте так: сначала спросите, нужен ли вообще весь
объект (может, хватит city: str), и только потом добавляйте делегирование.
без поведения?} B -- да --> Z1[Не нарушение.
Навигация по данным допустима] B -- нет --> C{Цепочка возвращает
тот же тип? fluent API} C -- да --> Z2[Не нарушение.
Builder / pipeline] C -- нет --> D{Нужен ли вызывающему
объект целиком?} D -- нет --> Z3[Лучший выход:
передать конкретное значение
data coupling] D -- да --> E{Решение принимается
по вытащенным данным?} E -- да --> Z4[Tell, Don't Ask:
перенести решение в объект] E -- нет --> Z5[Добавить делегирующий метод,
если он осмыслен в домене] Z5 --> F{Делегирующих методов
стало больше 3-4?} F -- да --> Z6[Сигнал: граница объекта
проведена неверно, пересмотреть модель] F -- нет --> Z7[Готово]
6. Как это выглядит в проде
6.1 Проверка связанности в CI
Дисциплина, которую нельзя проверить автоматически, умирает за один квартал. Поэтому границы модулей фиксируют машинно:
# Python: декларативные правила слоёв, падает в CI при нарушении
pip install import-linter && lint-imports # конфиг в .importlinter
# JavaScript / TypeScript: циклы и граф зависимостей
npx madge --circular --extensions ts,tsx src/
npx dependency-cruiser --validate .dependency-cruiser.js src
# Go: кто на кого ссылается, есть ли запрещённые импорты
go list -deps ./internal/domain | grep -v '^internal/domain'
# Java / Kotlin: правила слоёв как обычные unit-тесты (ArchUnit)
./gradlew test --tests '*ArchitectureTest*'
Пример правила для import-linter, запрещающего домену знать про инфраструктуру:
# .importlinter
[importlinter]
root_package = shop
[importlinter:contract:layers]
name = Слои: домен не знает про адаптеры
type = layers
layers =
shop.api
shop.application
shop.domain
[importlinter:contract:no-cycles]
name = Без циклов между пакетами
type = independence
modules =
shop.billing
shop.catalog
shop.shipping
Такой контракт — это исполняемая версия ADP и главной последовательности одновременно. Он ловит нарушение в момент коммита, а не через полгода на ретроспективе.
6.2 Связанность в Go: интерфейс определяет потребитель
Go даёт красивый пример структурного снижения связанности. Интерфейсы неявные, поэтому их объявляет тот, кто использует, а не тот, кто реализует:
// ПЛОХО: пакет storage экспортирует «толстый» интерфейс,
// все потребители зависят от 12 методов, а используют один-два.
package storage
type Repository interface {
Get(ctx context.Context, id string) (*Order, error)
Save(ctx context.Context, o *Order) error
Delete(ctx context.Context, id string) error
// ... ещё девять методов
}
// ХОРОШО: потребитель объявляет ровно то, что ему нужно.
// Ce пакета report = 1 метод, а не 12; моки в тестах тривиальны.
package report
type OrderFinder interface {
Get(ctx context.Context, id string) (*storage.Order, error)
}
func Build(ctx context.Context, f OrderFinder, id string) (Report, error) {
o, err := f.Get(ctx, id)
if err != nil {
return Report{}, fmt.Errorf("получение заказа %s: %w", id, err)
}
return Report{Total: o.Total}, nil
}
Больше об идиомах языка — в обзорной статье трека Go.
6.3 Связанность между сервисами
Всё вышесказанное масштабируется на распределённые системы, только цена ошибки умножается на сетевую границу и на организационную:
- Общая БД между сервисами — это common coupling в максимально дорогом варианте: схема таблицы становится неявным публичным API, менять её нельзя без координации всех.
- Синхронный вызов вместо события — временна́я коннасценция: сервис A не работает, пока лежит B.
- Закон Конвея работает в обе стороны: структура связей в коде стремится повторить структуру коммуникаций команд. Если два модуля постоянно меняются вместе разными командами — граница проведена неверно (Conway’s Law, martinfowler.com).
Хороший эмпирический сигнал, доступный любому проекту с git: temporal coupling — пары файлов, которые исторически меняются в одних и тех же коммитах. Адам Торнхилл описал метод в Your Code as a Crime Scene; посчитать его можно и вручную:
# частота совместных изменений файлов за последний год
git log --since='1 year ago' --name-only --pretty=format:'%H' \
| awk 'NF==0 {next} /^[0-9a-f]{40}$/ {c=$0; next} {print c" "$0}' \
| sort | uniq | awk '{print $2}' | sort | uniq -c | sort -rn | head -20
Файлы, которые всегда путешествуют парой, но лежат в разных модулях, — это либо пропущенная абстракция, либо неверно проведённая граница.
7. Типичные ошибки
Считать, что мало зависимостей = хорошая архитектура. Связанность можно обнулить копипастой: два модуля, ничего друг о друге не знающие, потому что код продублирован. Это худший обмен — см. разбор DRY в соответствующей статье. Полезная метрика не «количество связей», а «сколько мест надо менять на одно требование».
Прятать связанность вместо её устранения. Событийная шина, DI-контейнер, рефлексия, динамические импорты — всё это убирает связь из статического графа, но не из системы. Связь становится невидимой для компилятора и IDE, то есть хуже, а не лучше. Спрашивайте себя: изменение всё ещё вынуждает менять второе место? Тогда связь на месте, вы просто её ослепили.
Гнаться за низкой связанностью внутри одной команды и одного релиза. Внутри единицы деплоя связь дёшева: компилятор поймает, рефакторинг проедет по всему коду разом. Тратьте бюджет развязки на границы, которые нельзя изменить атомарно — между сервисами, между командами, в публичном API библиотеки.
Путать связность с размером. «Разбили класс на 800 строк на пять по 160» — это не про связность, это про строки. Если пять новых классов вызывают друг друга по цепочке и всегда меняются вместе, вы получили один класс, растянутый по пяти файлам, плюс новую связанность в довесок.
Буквальный LoD. Обёртки над обёртками, getUserNameFirstLetterUpper() в фасаде
над DTO. Признак: делегирующий метод не имеет смысла в языке домена. Останавливайтесь
и пересматривайте границу объекта.
Забывать про циклы. Циклы между пакетами не видны при чтении файла, но убивают модульность полностью. Проверка на ациклический граф — самая дешёвая проверка архитектуры, которую можно поставить в CI.
8. Мини-итог
- Связанность — цена изменения между модулями. Связность — осмысленность модуля. Это один принцип с двух сторон.
- У связанности есть шкала: content → common → external → control → stamp → data. Двигайтесь вниз по стоимости, а не по идеологии.
- У связности есть шкала: coincidental → logical → temporal → procedural →
communicational → sequential → functional.
utils.py— красный флаг по определению. - Коннасценция даёт более точный язык: сила, степень, локальность. Главное правило — чем дальше пересекается связь, тем сильнее её надо ослаблять.
- Метрики Ca/Ce/I/A/D и главная последовательность позволяют вести разговор о структуре цифрами, а ациклический граф зависимостей (ADP) — обязательное требование, а не идеал.
- Закон Деметры — операциональная эвристика: вызывай только соседей. Его цена — разрастание интерфейсов; сначала спроси, нужен ли вызывающему весь объект.
- Всё, что не проверяется в CI, деградирует. Ставьте import-linter / ArchUnit / dependency-cruiser в пайплайн.
Источники
- Larry Constantine, Edward Yourdon. Structured Design: Fundamentals of a Discipline of Computer Program and Systems Design, 1979 — первоисточник обеих шкал.
- Meilir Page-Jones. What Every Programmer Should Know About Object-Oriented Design, 1996 — коннасценция. Современное изложение: connascence.io.
- Karl Lieberherr, Ian Holland. Assuring Good Style for Object-Oriented Programs, IEEE Software, 1989 — страница проекта Demeter.
- Robert C. Martin. Clean Architecture, 2017 — компонентные принципы (REP, CCP, CRP, ADP, SDP, SAP) и метрики стабильности/абстрактности.
- John Ousterhout. A Philosophy of Software Design, 2018 — глубокие модули, change amplification, критика буквального LoD.
- Martin Fowler. TellDontAsk, ConwaysLaw, BoundedContext.
- Adam Tornhill. Your Code as a Crime Scene, 2015 — temporal coupling по истории git.
- Каталог рефакторингов на refactoring.guru — раздел «Обобщители связей» (Feature Envy, Inappropriate Intimacy, Message Chains).
- import-linter, ArchUnit, dependency-cruiser — инструменты.
Что дальше
Мы разобрались, как проводить границы между модулями. Следующий шаг — как выглядит код внутри этих границ: имена, размер функций, что считать хорошим комментарием и где проходит грань между «чисто» и «догматично».