Skip to content
PПромтбук
RUEN
04Архитектура

Ревью границ модулей

Где сейчас протекает абстракция, где модули знают слишком много друг о друге.

Ревью границ модулей в {{codebase}}.

Цель: понять что плохо разделено, какие модули знают слишком много друг о друге, и где это приводит к проблемам.

1. Карта зависимостей

Построй граф: модуль A импортирует модуль B.

Инструменты:

  • madge --image graph.svg src/
  • dependency-cruiser
  • Любой dep-graph визуализатор

Смотри:

  • Кто на ком завязан
  • Циклические зависимости (плохо!)
  • "Звёзды" — модули от которых зависит всё
  • Сироты — модули которые ни от кого не зависят

2. Признаки плохих границ

A. Циклические зависимости

auth ──→ user
 ↑        ↓
 └─── permissions

Эти модули нельзя поменять независимо. Слей или вынеси общий interface.

B. "Бог-модуль"

Один модуль импортится 50+ файлами и сам импортит 30+. Слишком много ответственности.

C. Прямое обращение к internals

// плохо — лезем в кишки
import { _internal_state } from './user/internals';

// хорошо — через публичный API
import { getCurrentUser } from './user';

D. Утечки типов

Если user.ts экспортит тип Database internals — модуль user знает про БД. Зачем?

E. Сквозные ID

Если order.userId, а в user.ts тоже order.userId — кто owner? Должна быть одна сторона.

3. Bounded contexts

Группируй модули в bounded contexts (как в DDD):

[Identity]
  - auth
  - user
  - session

[Billing]
  - subscription
  - invoice
  - payment

[Catalog]
  - product
  - category
  - inventory

Правила

  • Внутри context — могут зависеть друг от друга
  • Между context'ами — только через явный interface
  • ID — реплицируются, не разделяются (Billing.userId ≠ Identity.User, это просто ссылка)

4. Direction of dependencies

Зависимости должны идти в одну сторону. Снизу вверх:

high-level (use cases)
       ↓
mid-level (domain)
       ↓
low-level (infrastructure)

НЕ наоборот: domain не должен импортить ничего из infrastructure.

Для разворота направления — Dependency Inversion (интерфейс в верхнем слое, реализация в нижнем).

5. Тесты-проба

Хорошие границы дают:

  • ✓ Можешь юнит-тестить модуль без моков соседей
  • ✓ Можешь заменить модуль на in-memory fake
  • ✓ Можешь удалить модуль и понять что сломается из списка импортеров

Плохие границы:

  • ✗ Чтобы протестировать A, нужно стартануть B, C, D
  • ✗ Изменение в A ломает 20 файлов

6. Action items

| Проблема | Где | Почему плохо | Что делать |
|---|---|---|---|
| Циклическая зависимость auth ↔ user | src/auth, src/user | Нельзя менять независимо | Вынести User в /domain, auth и user импортят оттуда |
| Direct import internals | src/billing/index.ts:15 | Хрупко при рефакторе user | Использовать публичный getCurrentUser() |

7. Эволюция

Не пытайся сразу выправить всё. Приоритизируй:

  • P0: циклы (блокируют рефакторинг)
  • P1: прямые обращения к internals (хрупко)
  • P2: дублирование типов
  • P3: именования модулей

Один PR — одна граница. Не "пройдёмся по всему" — сломаешь всё.

Принципы

  • Хорошая граница — её не нужно объяснять
  • "А что если этот модуль выкинуть" — если ответ "ничего" или "одна вещь сломается" — граница чистая
  • Лучше явный дубль чем неявная зависимость
К подразделу «Архитектура»
Похожие промты