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

13. Стабильные идентификаторы

Имена стареют. Сервис Payments становится PaymentsService. Команда Orders.createOrder становится Orders.placeOrder. Целую подсистему переименовывают, когда команда переименовывает саму себя. В любой системе, где исходные файлы — каноническая модель, нужна идентичность, переживающая переименования; иначе любое переименование выглядит для инструментов сравнения как удаление плюс добавление.

Ответ ArchLang — стабильные идентификаторы: непрозрачные буквенно-цифровые суффиксы, закрепляющие идентичность независимо от имён.

service #k7m2qx Payments {
aspect team: "Payments"
rest_create authorize
}

#k7m2qx — стабильный идентификатор. Он есть у модуля; у интерфейса Authorize его нет. Эта глава объясняет оба решения.

Правило. Идентификатор должен быть бессмысленным и постоянным. Бессмысленным — потому что всё, что вы в него закодируете (домен, тип, аббревиатуру), рано или поздно перестанет соответствовать и будет соблазнять вас «исправить» его. Постоянным — потому что весь смысл идентификатора в том, чтобы нетронутым пережить любое переименование, смену области и перемещение. Никогда не меняйте идентификатор. Тот #k7m2qx с тем же успехом мог бы читаться как #q4w8zr — непрозрачность и есть достоинство.

Несёт #idНе несёт #id
МодулиПоверхности
ТипыИнтерфейсы
Процессы
Проекции
Подпроцессы
Политики
Запросы

Правило: всё, что можно переименовать независимо от контейнера. Модули можно переименовать; их идентичность нужно закреплять. Интерфейсы тоже можно переименовать, но они живут внутри модуля — их идентичность это «интерфейс Authorize внутри #k7m2qx», путь, который остаётся стабильным, пока стабильны и родительский модуль, и имя интерфейса. Когда переименовывается сам интерфейс, движок диффа использует эвристики по форме контракта и схожести имён.

Это компромисс. Идентификаторы на каждом интерфейсе обеспечили бы идеальное распознавание переименований ценой постоянного визуального шума в исходных файлах (каждый интерфейс в каждом модуле нёс бы непрозрачный префикс). Нынешнее распределение ставит идентификаторы туда, где они окупаются.

#a3f2b7e
#k7m2qx
#q4w8zr
  • #, за которым сразу идёт непрозрачный буквенно-цифровой суффикс.
  • Длина суффикса не ограничена; по соглашению 4–8 символов.
  • Чисто технические — никакого закодированного имени, никакого закодированного типа, никакой человекочитаемой основы.

«Никакого встроенного смысла» — в этом весь смысл. Кодирование чего бы то ни было в идентификатор — даже «это похоже на идентификатор payments» — подрывает стабильность: переименуйте систему из Payments в Settlements, и встроенная подсказка тут же устареет. Идентификаторы непрозрачны, чтобы в них нечему было стареть. (#pay001 или #orderschk были бы антипаттернами — они протаскивают домен в идентичность.)

Предпочитайте инструмент; запасной вариант от руки

Заголовок раздела «Предпочитайте инструмент; запасной вариант от руки»

Предпочитайте, чтобы идентификаторы создавал инструмент. Действительно случайные суффиксы трудно производить вручную, поэтому форматировщик создаёт его при первом сохранении:

// Вы пишете:
service Payments {
aspect team: "Payments"
}
// После сохранения форматировщик записывает:
service #k7m2qx Payments {
aspect team: "Payments"
}

Точный суффикс выбирается реализацией; относитесь к нему как к случайному.

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

service #payments-1 Payments { ... } // <имя-файла>-<индекс>
service #payments-9f3 Settlements { ... } // <имя-файла>-<случайный-постфикс>

Это будет выглядеть устаревшим как имя, как только модуль переедет в другой файл, — и это нормально. Идентификатор бессмыслен и никогда не меняется независимо от того, где живёт модуль; устаревший вид допустим, изменять его — нет. (Если найдёте лучший способ получить случайность от руки — используйте его; зерно из имени файла — это запасной вариант, а не идеал.)

Слот для идентификатора — между типом и именем:

service #k7m2qx Payments { ... }
process #h2k9p4 Checkout { ... }
view #z7q3w PaymentsLandscape { ... }
type #m4d8c module service { ... }

Для объявлений типов слот находится между type и родительским типом.

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

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

Почему поверхности и интерфейсы не несут идентификаторов

Заголовок раздела «Почему поверхности и интерфейсы не несут идентификаторов»

Если бы у поверхностей и интерфейсов были идентификаторы, визуально это выглядело бы так:

service #k7m2qx Payments {
surface #f823n PaymentsResource {
rest_create #cmd9c0 Authorize
rest_create #cmdbb2 Capture
}
}

Каждая строка получает непрозрачный префикс. Для сервиса с двадцатью интерфейсами это двадцать дополнительных лексем визуального шума на сервис.

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

Если ваша команда переименовывает интерфейсы постоянно, это может раздражать. Делать всё сразу (drop OldName + rest_create NewName { ... }) в одном коммите может скрыть непрерывность. Решение: переименовывать маленькими шагами.

Автоматически распространяемые подмодули

Заголовок раздела «Автоматически распространяемые подмодули»

Тела типов могут штамповать предзаполненные подмодули в каждый экземпляр:

type module service {
component metrics { rest_create emit } // каждый экземпляр service получает компонент 'metrics'
}

Компонент metrics появляется в каждом сервисе; вопрос в том, какой стабильный идентификатор он получает. Ответ: детерминированно выведенный, а не свеже созданный.

Формула: hash(parent_module_id + type_id + declared_subname), обрезанная до стандартной длины суффикса. Это делает идентификатор:

  • Стабильным при повторных сохранениях — один и тот же экземпляр производит один и тот же идентификатор metrics при каждом форматировании.
  • Одинаковым для экземпляров, выведенных из одного шаблона, — за исключением различия на экземпляр, вносимого parent_module_id.
  • Удобным для перекрёстных ссылок[[#derived-id]] на подмодуль, поставленный типом, разрешается одинаково каждый раз.

В исходниках вы это не увидите. Форматировщик создаёт эти идентификаторы, но они сидят на разрешённых узлах, а не в ваших файлах .arch.

Идентификаторы имеют область рабочего пространства. Коллизии внутри рабочего пространства не рекомендуются — форматировщик создаёт их так, чтобы их избегать, и инструменты предупреждают при обнаружении, — но они не фатальны. Дублирующиеся идентификаторы разрешаются в порядке объявления, и инструментарий отмечает конфликт.

Когда появятся библиотеки и внешние пакеты (будущая итерация), каждый пакет будет владеть собственным пространством имён идентификаторов; перекрёстные ссылки между пакетами будут уточняться пакетом.

  • Диффы видят переименования. Глава 14 посвящена этому: модуль с тем же #id и другим именем — это переименование, а не удаление плюс добавление. Именно стабильные идентификаторы делают изменение полноправным — рефакторинг читается как переименования и перемещения, а не «всё удалено и заново добавлено».
  • Перекрёстные ссылки остаются стабильными. [[#k7m2qx]] в описании продолжает работать после того, как Payments становится PaymentsService.
  • URL инструментария остаются стабильными. Внешние системы, ссылающиеся на конкретный модуль (<viewer>?focus=#k7m2qx), переживают каждое переименование модуля.
  • Идентификаторы должны быть бессмысленными и постоянными — непрозрачные буквенно-цифровые суффиксы, которые никогда не меняются и не кодируют ни домен, ни тип, ни семантику.
  • Идентификаторы несут модули, типы, процессы, проекции, подпроцессы, политики и запросы.
  • Поверхности и интерфейсы — нет; их идентичность — точечный путь внутри стабильного родительского модуля.
  • Предпочитайте форматировщик для создания случайных идентификаторов при сохранении; запасной вариант от руки — зерно на основе имени файла (<имя-файла>-<индекс>), что нормально, ведь идентификатор бессмыслен независимо от того, где окажется модуль.
  • Всё в долговечной модели несёт идентификатор; пропускайте их только пока набрасываете. Когда вы предлагаете изменения, у вещей должны быть идентификаторы.
  • Автоматически распространяемые подмодули получают детерминированно выведенные идентификаторы.
  • Компромисс: некоторое несовершенство эвристик при переименовании интерфейсов в обмен на чистый исходный код.

Глава 14: Диффы → — изменение как полноправный артефакт, построенный на системе идентификаторов из этой главы.