Интеграции почти всегда начинаются одинаково. Есть работающий адаптер к трекеру, нужен адаптер к мессенджеру — копируем папку, меняем имена, правим специфику. Это правильное решение для второй интеграции: контракт ещё не понятен, обобщать нечего.
К шестой интеграции это становится основным источником дефектов. Мы прошли этот путь и потратили заметную часть релиза на то, чтобы его развернуть.
Что накопилось
К моменту рефакторинга картина выглядела так:
| Было | Стало |
|---|---|
| Шесть копий приёма входящего события | Один входящий путь |
| Пять исполнителей опроса источников | Один runtime опроса |
| Четыре списка регистрации модулей | Один composition root |
| Ссылки на секреты разбирались в каждом адаптере | Один владелец ссылок на секреты |
| Каталог типов жил отдельно от кода | Проекция контракта |
| Роль в чате — строковые литералы | Роль объявляет сам адаптер |
| Представление для интерфейса размазано по слоям | Один владелец представления |
Каждая строка здесь — не эстетическая претензия, а конкретный класс инцидентов.
Шесть копий приёма входящего означают, что исправление дедупликации приезжает в одну интеграцию и не приезжает в пять остальных. Четыре списка регистрации — что новый адаптер работает в интерфейсе, но не работает в опросе, потому что его забыли в третьем списке. Строковые литералы для роли — что опечатка выясняется в рантайме.
Контракт вместо каталога
Самое полезное изменение оказалось самым неочевидным: каталог типов интеграций перестал быть отдельной сущностью и стал проекцией контракта.
Раньше это были две независимые вещи: код адаптера и описание того, что этот адаптер умеет, — в отдельном списке, который надо не забыть обновить. Расхождение между ними обнаруживалось пользователем.
Теперь описание выводится из самого контракта. Адаптер объявляет свои возможности, свою роль в чате, свою схему настроек — и интерфейс, документация и валидация читают одно и то же. Рассинхронизироваться нечему.
Отсюда же выросла способность отдавать структурное описание интерфейса модели: если адаптер описывает себя сам, это описание можно передать агенту, и он будет работать с фактическим контрактом, а не с предположениями о нём.
Что появилось вместе с единым контуром
Когда входящий путь один, на нём можно поставить вещи, которые раньше пришлось бы ставить шесть раз.
Защита от циклов исходящих сообщений. Бот отвечает в канал, его собственное сообщение приходит обратно как событие, он отвечает снова. Пока путей приёма шесть, такая защита обязательно где-то отсутствует.
Корреляция и контекст наблюдения. Каждая операция адаптера несёт контекст, по которому её видно в телеметрии. Единый контур означает, что это не нужно вспоминать в каждом адаптере отдельно.
Живые статусы очередей. Интерфейс показывает фактическое состояние опроса, а не заранее записанное «в порядке». Отдельное неприятное открытие рефакторинга: старый интерфейс показывал зелёное там, где давно было красное.
Вывод интеграции из эксплуатации. Раньше отключение означало «удалить запись в базе и надеяться». Теперь это полноценная операция с очисткой — потому что есть единое место, которое знает, что именно надо очистить.
Политика хостов. Для трекера — явный список разрешённых адресов вместо «куда настроили, туда и пойдём».
Побочное следствие: онбординг делает человек
Отдельный вывод, который стоит зафиксировать. В какой-то момент у нас интеграции мог создавать агент-воркер: увидел задачу, завёл подключение, пошёл работать.
Звучит удобно ровно до первого случая, когда агент заводит подключение к трекеру с некорректным адресом — и это уходит в рабочий контур. Создание интеграции — это конфигурация периметра, а не рабочая операция. Такое делает человек.
Мы вынесли это в явное правило: воркер работает внутри выданных доступов и не расширяет их сам. Расширение прав — отдельная операция с записью в журнал аудита и подтверждением владельца.
Что делать, если вы на пятой копии
Практический порядок, который сработал у нас:
- Сначала входящий путь. Он самый повторяемый и даёт наибольшую отдачу — дедупликация, корреляция и защита от циклов оказываются в одном месте.
- Потом регистрация. Один composition root вместо списков. Это скучно и механически, но убирает целый класс «работает наполовину».
- Потом секреты. Один владелец ссылок на секреты — предпосылка к тому, чтобы вынести их в защищённое хранилище, а не держать в переменных окружения.
- Контракт — в конце. К этому моменту уже видно, что у адаптеров действительно общего, и обобщение получается по факту, а не по догадке.
И главное: если у вас есть адаптер, написанный копированием соседней папки, его придётся переписать под контракт. Копия не мигрирует сама, а попытка поддержать оба способа возвращает вас к исходной задаче — только теперь с двумя контурами вместо шести.