Принципы разработки Связанность, связность и закон Деметры
0%

Связанность, связность и закон Деметры

Связанность, связность и закон Деметры

Если из всей теории проектирования вам разрешат унести с собой ровно две идеи — берите эти две. Низкая связанность (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 — «все валидации», «все хендлеры», где нужная ветка выбирается по флагу. Coincidentalutils.py, helpers.py, common/: сюда кладут то, чему не нашли места. Это худшая связность, и её симптом узнаётся мгновенно: имя модуля нельзя сузить — про что utils? Про всё, значит ни про что.

Практический тест на связность формулируется как вопрос: «Какое изменение требований заставит меня открыть этот файл?» Если ответов один-два — связность высокая. Если десяток разнородных — модуль надо резать. Это, по сути, формулировка Принципа единственной ответственности (SRP) в терминах связности; подробнее — в статье про SOLID.


3. Коннасценция: более точный язык

Шкалы Константина хороши, но грубоваты: они говорят «это control coupling», но не говорят, насколько это больно и что делать. Мейлир Пейдж-Джонс в What Every Programmer Should Know About Object-Oriented Design (1996) предложил более тонкую модель — коннасценцию (connascence, «со-рождённость»). Два фрагмента кода коннасцентны, если изменение одного требует изменения другого, чтобы система осталась корректной.

Модель даёт три оси оценки — сила, степень и локальность, — и это её главная ценность: сравнивать можно не только «что», но и «насколько плохо».

Три правила работы с коннасценцией (сам Пейдж-Джонс формулирует их как рекомендации):

  1. Минимизируй общую коннасценцию, разбивая систему на инкапсулированные части.
  2. Минимизируй коннасценцию, пересекающую границы: внутри класса связь имени — бесплатна, между сервисами — катастрофа.
  3. Максимизируй коннасценцию внутри границ — это и есть высокая связность.

Ключевой практический вывод: связь тем допустимее, чем она локальнее. Коннасценция позиции (порядок аргументов) внутри приватного метода на две строки — вообще не проблема. Она же между двумя сервисами разных команд (порядок полей в бинарном формате) — инцидент, ждущий своего часа.

# Коннасценция позиции: вызывающий обязан помнить порядок
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 может вызывать только методы:

  1. самого объекта O (включая унаследованные);
  2. объектов, переданных M как аргументы;
  3. объектов, которые M создал сам;
  4. объектов, являющихся полями (компонентами) 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), и только потом добавляйте делегирование.


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 — инструменты.

Что дальше

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

Чистый код: имена, функции, границы, комментарии

Нашли неточность? Выделите фрагмент текста — рядом появится жучок.

Нужен разбор именно вашей ситуации?

Статья описывает общий случай. Если у вас частный — можно разобрать его отдельно, платно. А если не хватает целого материала, предложите тему: её оплачивают вскладчину, и она выходит открытой для всех.

Доска запросов