Перейти к содержимому

14. Диффы

Каждая модель ArchLang живёт в git, и это делает изменение полноправным: ветки — это будущие состояния, коммиты — архитектурные решения, рабочее дерево — настоящее. Стабильные идентификаторы (Глава 13) закрепляют идентичность сквозь всё это, так что рефакторинг читается как переименования и перемещения, а не «всё удалено и заново добавлено». Интересная часть — как ArchLang превращает построчные диффы git в структурные диффы — диффы, говорящие о модулях, интерфейсах и процессах, а не о добавленных и удалённых строках.

Эта глава о том, как это работает, что с этим можно делать и в чём ограничения.

Дифф git двух файлов .arch показывает добавленные и удалённые строки. Это полезно для чтения файла, но бесполезно для чтения архитектуры. Переименуйте модуль из Payments в PaymentsService, и дифф git покажет:

service #k7m2qx Payments {
service #k7m2qx PaymentsService {

Ревьюер может это прочитать и понять, что это переименование. Но автоматизированному инструментарию нетривиально сказать «переименование Payments не вводит новых зависимостей» — для этого нужно знать, что это переименование, а не удаление плюс добавление.

Структурный дифф говорит примерно так:

  • Переименовано: PaymentsPaymentsService (тот же #k7m2qx).
  • Добавлен аспект: criticality: "High" на PaymentsService.
  • Добавлен интерфейс: PaymentsService.void.
  • Добавлен модуль: FraudCheck (#r3n8wt).
  • Удалён модуль: LegacyBilling (#b9x4cd).
  • Изменён процесс: BasicPayment — вставлен один шаг.

Глава 3 прошлась по одному из таких диффов целиком. Эта глава фокусируется на механике и инструментарии.

Движок диффа сопоставляет старое с новым в таком порядке:

  1. По стабильному идентификатору. Всё, что имеет #id, сопоставляется по идентификатору. Модуль с #k7m2qx в старом файле сопоставляется с модулем #k7m2qx в новом, независимо от имени. Здесь определяются переименования.
  2. По структурному пути внутри стабильного родителя. Поверхности и интерфейсы не несут идентификаторов (Глава 13). Их идентичность — точечный путь внутри родителя, который их несёт: Payments.authorize сопоставляется с PaymentsService.authorize, потому что родитель #k7m2qx сопоставился первым.
  3. По эвристикам формы контракта. Внутри сопоставленного родителя интерфейсы с разными именами проверяются на схожесть — тип, поля, описание, схожесть имён. Сильные совпадения сообщаются как переименования; слабые откатываются к удалению плюс добавлению.

Режим диффа просмотрщика рендерит структурную дельту прямо на диаграмме. Интерфейс просмотрщика предлагает несколько сохранённых проекций диффа:

  • Было / Стало — добавленные узлы окрашиваются в зелёный, удалённые в красный, изменённые в жёлтый.
  • Что изменилось — только список, без диаграммы, просто маркированная форма структурной дельты.
  • Новые зависимости — фокус на рёбрах, введённых диффом. Полезно, когда рефакторинг добавляет вызовы между командами и хочется убедиться, что они намеренные.
  • Удалённые зависимости — обратное. Показывает то, что раньше общалось, а теперь нет.

Каждая — это просто настроенный рендер диффа; движок один и тот же.

У диффа две стороны. Это могут быть:

  • Две git-ревизии в одной ветке — например, «что изменилось в последнем коммите?».
  • Две ветки — что фича-ветка вводит поверх main.
  • Рабочее дерево против HEAD — незакоммиченные изменения.
  • Два произвольных каталога — пример из Главы 3.

Все четыре сводятся к двум разобранным снимкам; один и тот же движок их сравнивает. Разные поверхности инструментария (размещённый просмотрщик, плагины редакторов, скрипты, использующие основную библиотеку диффа) предоставляют разные удобные точки входа для выбора двух сторон.

Старые средства моделирования используют поля-маркеры — state: as-is, state: to-be, new, changed, existing — чтобы указать, что модель содержит и текущую, и предлагаемую структуру. ArchLang в них не нуждается.

Эту роль играют ветки. Ваша основная ветка — это текущая архитектура. Фича-ветка — это предлагаемая архитектура. Дифф между ними — «что изменится, если мы внедрим это предложение». Ревьюеры видят структурную дельту и принимают решение.

Никаких флагов. Никакой второй копии того же модуля с другими аннотациями. Нет риска забыть удалить маркер state: to-be после внедрения.

  • Переименования интерфейсов вместе со сменой типа могут победить эвристику. rest_create authorizerest_read validate в одном коммите выглядит как удаление плюс добавление. Смягчите, разделив изменение на два коммита (сначала переименование, потом смена типа).
  • Перемещения интерфейсов между модулями в настоящее время трактуются как удаление плюс добавление. Если вы переместите OrderEvents из Orders в EventBus, дифф скажет, что одно событие удалено, а другое добавлено. Будущая итерация может отслеживать такие случаи.
  • Переименования целого пакета требуют ручной заботы. Имя пакета в package.archspace не несёт идентификатора; если переименовать acme.shop в acme.commerce, загрузчик трактует результат как новый пакет. На практике большинство переименований происходит в каталогах и путях зависимостей, где разрешение зависимостей по пути это допускает.

Поскольку движок диффа уважает стабильные идентификаторы и точечные пути, крупные рефакторинги остаются ревьюабельными:

  • Разделение модуля на два: переместите половину интерфейсов в новый модуль с новым идентификатором. Дифф покажет: один интерфейс удалён из OldModule, добавлен один новый модуль, содержащий эти интерфейсы. Ревьюеры видят перемещение; эвристика по форме интерфейса связывает перемещённые интерфейсы с их прежними определениями.
  • Реорганизация вложенности: меняем service Orders { ... } на in CommerceSystem service Orders { ... }. Идентификатор модуля остаётся прежним; дифф сообщает о смене родителя, а не об удалении.
  • Переименование аспектов: меняем domain: "Payments" на domain: "PaymentDomain" во многих файлах. Дифф сообщает об изменениях значений полей по каждому модулю; структурная форма не затронута.

Движок диффа доступен как функция библиотеки в @archlang/engine. Конвейеры и инструменты ревью потребляют его структурированный вывод — список переименований, добавлений, удалений и модификаций — и подают в:

  • Автоматизацию ревью кода (блокировать PR, вводящие межкомандные зависимости без согласования).
  • Записи архитектурных решений (автогенерировать разделы «что изменилось» в ADR).
  • Отчёты о соответствии требованиям (любая модификация, затрагивающая модуль с security.zone: PCI, помечается для проверки).

Контрольная точка для повседневного читателя

Заголовок раздела «Контрольная точка для повседневного читателя»

Если вы потребляете только типы, определённые вашей стандартной библиотекой или платформенной командой, у вас уже есть полная картина:

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

Здесь можно остановиться. Оставшиеся главы посвящены определению метамодели — созданию собственных типов, управлению тем, как распространяются их поля, и созданию пользовательских виджетов. Они существенны для платформенных команд и авторов расширений языка. Они необязательны для прикладных команд, потребляющих типы стандартной библиотеки.

  • Дифф ArchLang структурный — он говорит о переименованных модулях и добавленных интерфейсах, а не о добавленных строках.
  • Стабильные идентификаторы позволяют диффу распознавать переименования; интерфейсы используют эвристики формы контракта.
  • Просмотрщик поставляет несколько проекций диффа: Было/Стало, Что изменилось, Новые/Удалённые зависимости.
  • Ветки заменяют маркеры TO-BE / AS-IS; ревью PR — это ревью архитектурной дельты.
  • Движок диффа доступен как функция библиотеки для конвейеров и инструментов ревью.

Глава 15: Зачем типы? → — первая глава Части IV и сдвиг мышления, после которого остальной язык обретает смысл.