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

Fitness Functions и эволюционная архитектура

Fitness function — автоматизированный тест того, что архитектурное свойство по-прежнему выполняется: wrong-way detector, который ломает сборку при нарушении, не позволяя архитектуре дрейфовать под давлением дедлайнов.

ARCH Senior ◷ 22 min
Уровень
ОсновыJuniorMiddleSenior

Через восемнадцать месяцев после того, как B2B биллинговая платформа выстроила modular monolith с принудительными границами, команда наняла двенадцать новых инженеров. Правила модулей были в ADR и в онбординговой документации. Правила ESLint для ограничения импортов существовали — но никто не проверял их четыре месяца, поскольку шаг линтинга был случайно удалён из CI-пайплайна при рефакторинге Dockerfile. Никто не заметил, потому что сборка продолжала выдавать зелёный результат.

Когда кто-то запустил архитектурные тесты вручную, обнаружилось 23 нарушения границ в шести модулях. Billing импортировал из внутренностей ordering. Модуль notifications напрямую вызывал слой репозитория модуля inventory. Хуже того: нарушения были задеплоены в продакшн. Архитектурная структура, которую команда строила месяцами, была разрушена — молча, инкрементально, одним ярлыком за другим — пока CI-пайплайн сообщал о зелёном статусе.

Проблема была не в правилах. Проблема была в том, что ничто не измеряло непрерывно, выполняются ли правила по-прежнему.

Что такое fitness function

Термин пришёл из книги Нила Форда, Ребекки Парсонс и Патрика Куа «Building Evolutionary Architectures» (O’Reilly, 2017). Они позаимствовали его из эволюционных вычислений, где fitness function оценивает, насколько хорошо кандидат-решение удовлетворяет требованиям задачи.

В архитектуре fitness function — любой механизм, обеспечивающий объективную, автоматизированную оценку того, выполняется ли архитектурная характеристика по-прежнему. Это не документация. Это не чеклист для code review. Это запускается в CI и выдаёт pass или fail.

Архитектурные характеристики — иногда называемые «-ilities» — это нефункциональные свойства, которые архитектура должна сохранять:

  • Модульность: ни один модуль не импортирует из внутренностей другого
  • Задержка: p99 времени ответа billing API не превышает 200 мс
  • Отсутствие циклических зависимостей: нет циклов между модулями на уровне пакетов
  • Безопасность: чувствительные данные не попадают в логи
  • Масштабируемость: система может обработать 10 000 генераций инвойсов в минуту

Fitness function превращает эти характеристики из пожеланий в гейты сборки.

Atomic vs holistic fitness functions

Форд, Парсонс и Куа выделяют два измерения fitness functions: область охвата и частота.

Atomic fitness functions тестируют одну архитектурную характеристику изолированно:

  • «Нет циклических зависимостей между модулями» — одно правило ArchUnit
  • «Нет прямых импортов из внутреннего пакета другого модуля» — правило ts-arch или ESLint
  • «Все методы публичного API возвращают ответ в пределах 50 мс в unit-тестах» — assertion производительности в тест-сьюте

Holistic fitness functions тестируют комбинацию характеристик, проявляющихся только совместно:

  • «При 10 000 RPM система поддерживает p99 < 200 мс И частоту ошибок < 0,1% И отсутствие OOM» — нагрузочный тест в CI
  • «Задеплоенная система проходит сканирование безопасности И аудит зависимостей И отсутствие кросс-граничного доступа в продакшн-трейсах» — составной гейт

Большинство команд начинают с atomic fitness functions, потому что они дёшевы в написании и быстры в выполнении. Holistic functions дороже, но выявляют эмерджентные режимы отказа, которые атомарные тесты пропускают.

Why this works

Почему это называется «evolutionary architecture»? Потому что fitness functions обеспечивают управляемое инкрементальное изменение. Без fitness functions архитектура деградирует через накопленные ярлыки — каждое отдельное изменение мало и объяснимо, но суммарный эффект — разрушение. С fitness functions каждое изменение оценивается против архитектурных свойств, которые команда взяла на себя обязательство поддерживать. Архитектура может эволюционировать — добавляются новые паттерны, убираются старые — но только в направлениях, сохраняющих важные свойства. Fitness function делает изменение безопасным. Это разница между «мы эволюционировали архитектуру намеренно» и «архитектура дрейфовала, пока кто-то не заметил».

Викторина

Архитектурная команда биллинговой платформы хочет добавить fitness function для предотвращения нарушений границ. Есть два предложения: (A) Добавить тест ArchUnit, утверждающий «ни один класс в billing.internal не может импортировать из ordering.internal» — запускается в unit-тест-сьюте при каждом коммите. (B) Добавить архитектурный чеклист, который старшие инженеры заполняют в каждом PR. Что из этого является fitness function и почему это важно?

Triggered vs continuous fitness functions

Второе измерение по Форду/Парсонс/Куа — частота:

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

Continuous fitness functions запускаются при каждом изменении, в цикле разработки, максимально быстро. Подходят для структурных проверок (правила зависимостей, циклические зависимости, ограничения импортов), которые можно вычислить по исходному коду без сборки или деплоя.

Для проблемы нарушений границ биллинговой платформы правильны continuous fitness functions: проверка ArchUnit или ts-arch запускается в unit-тест-сьюте при каждом коммите, обнаруживая нарушение в момент его введения. Нет задержки между нарушением и обнаружением.

Нагрузочный тест, верифицирующий p99 < 200 мс при 10 000 RPM, по необходимости triggered — он требует живого окружения и занимает минуты. Запускается ежесуточно или перед релизом, а не при каждом коммите.

Викторина

У команды биллинга три предложенные fitness functions: (1) Правило ArchUnit, утверждающее отсутствие циклических зависимостей между модулями — занимает 2 секунды в тест-сьюте. (2) Нагрузочный тест, утверждающий p99 < 150 мс при 5 000 конкурентных запросов инвойсов — занимает 8 минут против стейджингового окружения. (3) Сканирование уязвимостей зависимостей — занимает 30 секунд. Как их классифицировать как triggered vs continuous и с какой частотой запускать каждую?

Реализация fitness functions: практические примеры

ArchUnit (Java) — направление зависимостей:

// Тест падает, если любой класс в billing импортирует из ordering.internal
@AnalyzeClasses(packages = "com.platform")
class ModuleBoundaryTest {
  @ArchTest
  static final ArchRule billingMayNotImportOrderingInternals =
    noClasses().that().resideInAPackage("..billing..")
      .should().dependOnClassesThat()
      .resideInAPackage("..ordering.internal..");
}

ts-arch (TypeScript) — отсутствие циклов:

// Тест падает, если существует циклическая зависимость между модулями
describe("architecture", () => {
  it("has no cyclic module dependencies", async () => {
    const result = await tsarch.FileSystem
      .fromConfig("tsconfig.json")
      .onlyFiles(["src/**/*.ts"])
      .noCycles();
    expect(result).toEqual([]);
  });
});

Бюджет задержки (assertion в unit-тесте):

it("invoice generation completes within 100ms", async () => {
  const start = performance.now();
  await billingService.generateInvoice(testOrderId);
  const elapsed = performance.now() - start;
  expect(elapsed).toBeLessThan(100);
});

Общее: каждый — тест, выдающий pass/fail. Каждый запускается в CI. Каждый ловит конкретное архитектурное свойство. Свойство ранее поддерживалось только человеческой конвенцией; fitness function превращает его в машинно-применяемый инвариант.

lesson.inset.note

Нарушения границ на биллинговой платформе были обнаружены в конечном счёте — но только после месяцев нахождения в продакшне. Стоимость исправления была пропорциональна тому, как долго они существовали: шесть модулей имели нарушения, некоторые несущие нагрузку. Запускайся fitness function непрерывно, каждое нарушение было бы обнаружено в течение часов после введения, когда задействовано изменение только одного инженера. Стоимость исправления нарушения — O(1) при немедленном обнаружении; она растёт примерно линейно с количеством изменений, наложенных поверх него.

Викторина

После исправления 23 нарушений границ команда рассматривает добавление fitness functions. Джуниор-инженер предлагает: «Можно добавить fitness functions прямо сейчас, без рефакторинга нарушений? Тогда они будут падать, и мы будем исправлять по ходу». Почему это рассуждение неверно?

Вспомните перед уходом
  1. 01
    Что такое fitness function и чем она отличается от чеклиста code review или архитектурного документа?
  2. 02
    В чём разница между atomic и holistic fitness functions? Приведи пример каждой.
  3. 03
    Почему «evolutionary architecture» требует fitness functions, а не просто хорошей документации и ревью старших инженеров?
Итог

Молчаливая деградация биллинговой платформы — 23 нарушения границ, накопленные за месяцы при зелёном CI — это режим отказа, который fitness functions предотвращают. Нарушения вводились не небрежными инженерами; их вводили инженеры, не получавшие немедленной обратной связи о пересечении границы.

Fitness function превращает архитектурное намерение в машинно-применяемый инвариант. Тест ArchUnit не доверяет инженерам помнить правила; он ловит нарушения в момент их введения — до любого code review, до любого деплоя, пока изменение ещё локальное и дешёвое для исправления.

Evolutionary architecture — не архитектура, которая дрейфует. Это архитектура, изменяющаяся намеренно, под руководством fitness functions, определяющих свойства, достойные сохранения. Fitness function дополняет ADR: ADR фиксирует, почему свойство было выбрано; fitness function применяет, что свойство по-прежнему поддерживается.

Практическая последовательность: написать ADR для фиксации решения, реализовать fitness function для его применения, запускать fitness function непрерывно для обнаружения дрейфа. Когда fitness function начинает падать по принципиальной причине — силы изменились, новый паттерн заменяет старый — написать новый ADR для superseding исходного, обновить fitness function и продолжить.

Практика

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

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

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

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

Примени это

Примени этот урок в реальном проекте.

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

Trademarks belong to their respective owners. Editorial reference only.