Перейти к основному контенту

Стандарт компонентной и плагинной разработки Metabot v1.0

Инженерные принципы разработки плагинов и модулей Metabot

Зачем это нужно

Metabot развивается как платформа, а не как набор разрозненных скриптов, временных обходов и локальных helper-классов. Поэтому для нас критично не просто “написать рабочий код”, а делать это так, чтобы решения можно было повторно использовать, безопасно развивать, тестировать, документировать и передавать между разработчиками без потери смысла.

Этот стандарт нужен, чтобы:

  • уменьшать архитектурный шум и техдолг;
  • не превращать платформу в свалку случайных утилит;
  • проводить границу между локальным решением и системным компонентом;
  • не смешивать бизнес-логику, интеграции, канальные адаптеры и инфраструктурные детали;
  • делать новые возможности платформы предсказуемыми для команды, сценаристов и интеграторов.

Главная идея простая: мы не пишем код “под случай”, мы строим расширяемую инженерную среду.


1. Сначала короткий spec, потом код

Принцип

Любая новая платформенная доработка начинается с короткого спека: контекст, цель, границы изменения, ограничения, критерии приёмки.

Почему это важно

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

  • что именно меняется;
  • где граница ответственности;
  • что нельзя сломать;
  • как проверяется результат.

Плохо

“Надо быстро подправить два места, чтобы заработало.”

Хорошо

“Нужно ввести общий outbound HTTP-примитив с proxy/retry и перевести на него конкретные точки, не ломая старые одноаргументные вызовы.”

Пример

Если появляется системный модуль для исходящих HTTP-запросов, он должен начинаться не с куска кода, а со спека: где он используется, что считается успехом, какие ошибки retryable, как он доставляется в V8 и как тестируется.


2. Один модуль — одна причина к изменению

Принцип

У класса, сервиса или плагина должна быть одна понятная причина измениться.

Почему это важно

Если модуль одновременно:

  • делает HTTP-запросы,
  • конвертирует CSV,
  • сохраняет файлы,
  • определяет MIME,
  • проверяет политику безопасности,
  • генерирует временные имена,

то он уже не является компонентом. Это комбайн. Такие модули плохо тестируются, плохо переиспользуются и становятся центром роста техдолга.

Плохо

Один Request-класс, который “умеет всё”.

Хорошо

Отдельные компоненты:

  • outbound HTTP;
  • file transfer;
  • CSV utilities;
  • event adapters;
  • policy/context validation.

Пример

Компонент голосового ввода должен отвечать за голосовой ввод, а не одновременно за Telegram payload, загрузку аудио, транскрибацию, файловое хранилище и обработку бизнес-сценария.


3. У каждого компонента должен быть явный контракт

Принцип

У компонента должны быть:

  • понятные входы;
  • понятные выходы;
  • предсказуемое поведение;
  • задокументированная семантика ошибок и ограничений.

Почему это важно

Компонент без контракта быстро превращается в “магическую штуку”, которую кто-то когда-то написал, а остальные боятся трогать. Это ломает повторное использование и делает развитие платформы случайным.

Плохо

Метод с названием getFileInfoByUrl(), который в одних случаях только возвращает метаданные, а в других ещё скачивает файл, создаёт temp file и вычисляет MIME по содержимому.

Хорошо

  • fetchUrlContents() — получает контент;
  • getFileInfoByUrl() — получает метаданные;
  • downloadFileFromUrlToBusiness() — скачивает и сохраняет файл в хранилище.

Пример

Событие ticket_status_changed должно явно документировать доступные поля, а не оставлять разработчика угадывать, есть ли там предыдущий статус, текущий статус или только ticket_id.


4. Неявная передача данных запрещена

Принцип

Компоненты не должны зависеть от скрытых связей, случайных значений в памяти, жёстко заданных путей, доменов, записей в БД или “магии рантайма”, если это явно не является частью контракта.

Почему это важно

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

Плохо

  • жёсткий путь к папке на диске;
  • жёстко заданный домен в коде;
  • предположение, что JS-обёртка “как-нибудь увидит” PHP-модуль;
  • использование значения, которое другой скрипт когда-то где-то записал.

Хорошо

  • пути и домены берутся из конфигурации;
  • зависимости передаются явно;
  • механизм загрузки модулей описан и воспроизводим;
  • мост между PHP, JS и V8 является частью documented delivery model.

Пример

Платформенный модуль не должен знать, что файлы конкретного бота лежат в конкретной директории конкретного сервера. Это не обязанность модуля.


5. Канальная логика должна быть спрятана внутри компонента

Принцип

Сценарии и верхнеуровневая логика должны описывать что происходит, а не вручную реализовывать Telegram, Max, webhook-переходы, форматы multipart или повторные попытки запросов.

Почему это важно

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

Плохо

Сценарий сам знает, как устроен Telegram file API, как формируется URL, как грузится файл через прокси и как повторять запрос.

Хорошо

Сценарий обращается к компоненту:

  • VoiceInput
  • ProxyFetch
  • ChannelArtifactResolver
  • TicketStatusChangeEvent

Пример

Если платформа начинает работать и с Telegram, и с Max, и с другими каналами, различия между ними должны жить в адаптерах и компонентах, а не размазываться по V8-скриптам.


6. Общий платформенный код не должен зависеть от конкретного проекта

Принцип

Если код претендует на статус общего системного компонента, он должен быть переносимым и пригодным для повторного использования.

Почему это важно

Компонент, который работает только в одном проекте, на одном домене, в одном окружении или с одним bot ID, не является компонентом платформы. Это проектный helper.

Плохо

  • жёстко заданные пути;
  • жёстко заданные URL;
  • привязка к конкретному боту, инстансу, бизнесу или окружению;
  • хранение инфраструктурной информации внутри логики модуля.

Хорошо

  • конфигурация выносится наружу;
  • файловые пути вычисляются через storage layer;
  • URL строятся через общие сервисы;
  • компонент можно использовать повторно без переписывания.

Пример

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


7. Legacy не расширяем дальше как новый стандарт

Принцип

Если старый модуль уже используется во многих местах, это не значит, что его нужно продолжать развивать как основной архитектурный центр.

Почему это важно

Legacy-код может быть полезен как compatibility layer, но если в него продолжать складывать новые обязанности, он становится точкой системного разложения.

Плохо

Ради новой задачи расширять старый helper ещё сильнее, потому что “он и так уже везде используется”.

Хорошо

  • новый системный слой вводится отдельно;
  • старый модуль признаётся legacy;
  • миграция идёт постепенно;
  • новые доработки делаются на новом стандарте.

Пример

Если есть старый модуль, который уже умеет и HTTP, и файлы, и CSV, и multipart, это не повод использовать его как базу для новой платформенной логики. Это повод остановить его рост и начать выносить отдельные системные примитивы.


8. Сначала системный примитив, потом точечная миграция

Принцип

Повторяющиеся инженерные проблемы должны решаться один раз как системный примитив, а не много раз в разных helper-классах.

Почему это важно

Когда прокси, retry, таймауты, fallback-логика и диагностика реализуются в разных местах по-разному, это не ускорение, а размножение будущих багов.

Плохо

  • отдельно прокси для Telegram;
  • отдельно retry для файлов;
  • отдельно download helper в интеграционном плагине;
  • отдельно ещё один wrapper “на всякий случай”.

Хорошо

  • единый outbound HTTP-слой;

  • единые правила proxy/retry/timeout;

  • поверх него — разные политики:

    • generic fetch,
    • file info,
    • strict file download.

Пример

Если проблема в file_get_contents без прокси возникает в нескольких местах, правильное решение — не “пропатчить ещё одну точку”, а ввести платформенный способ исходящих HTTP-запросов и постепенно перевести туда нужные вызовы.


9. Границы контекстов должны быть названы

Принцип

У каждого модуля и сервиса должен быть понятный контекст: о чём он и о чём он не должен знать.

Почему это важно

Без границ всё начинает прилипать ко всему. HTTP начинает знать про файлы, файлы — про каналы, каналы — про бизнес-правила, а бизнес-правила — про способы доставки JS-модулей.

Плохо

Один модуль сразу “про интеграции”, “про файлы”, “про экспорт”, “про CSV”, “про загрузку”, “про политики” и “про события”.

Хорошо

Отдельные контексты:

  • Outbound HTTP
  • File Transfer
  • Event Contracts
  • CSV/Artifacts
  • Channel Adapters
  • Runtime Delivery

Пример

ticket_status_changed — это контекст событий и контрактов. ProxyFetch — это контекст исходящего HTTP. downloadFileFromUrlToBusiness() — это контекст скачивания и сохранения файла. Смешивать это в одну сущность нельзя.


10. Любая платформенная фича обязана приехать вместе с четырьмя хвостами

Принцип

Код без доставки и сопровождения не считается завершённой фичей.

Минимальный комплект

  • spec;
  • changelog;
  • docs;
  • тестовый сценарий.

Почему это важно

Если код есть в git, но:

  • runtime его не видит;
  • V8 не находит модуль;
  • никто не знает, как им пользоваться;
  • он не отражён в версии;

то это не готовая функциональность, а полуфабрикат.

Пример

Новый системный модуль должен:

  • быть описан в specs;
  • попасть в changelog версии;
  • иметь документацию по подключению и использованию;
  • быть реально проверен на stage.

11. Перед началом реализации команда должна ответить на пять вопросов

Обязательные вопросы

  1. Какова цель изменения?
  2. Где проходит граница модуля или сервиса?
  3. Какие контракты меняются?
  4. Какие инварианты нельзя нарушить?
  5. Как проверяется результат?

Почему это важно

Если на эти вопросы нет ответа, значит команда ещё не проектирует систему, а только реагирует на симптомы.

Пример

Если в событие смены статуса добавляется previous_status_id, это не “маленькая доработка”. Это изменение event contract. Значит, нужно заранее понять:

  • кого оно затронет;
  • как это будет документировано;
  • что останется backward-compatible;
  • как будет выглядеть payload события после изменения.

Практические антипаттерны

Антипаттерн: “универсальный helper”

Признаки

  • делает слишком много;
  • его имя уже не соответствует содержанию;
  • в него продолжают складывать новые обязанности;
  • его боятся трогать, но продолжают использовать.

Что делать

  • перестать расширять;
  • зафиксировать как legacy;
  • выделить новые системные примитивы.

Антипаттерн: “разовый фикс становится стандартом”

Признаки

  • решение делалось локально;
  • потом на него начинают ссылаться как на общий подход;
  • контракта и docs нет;
  • оно не готово к повторному использованию.

Что делать

  • либо честно оставить это локальным кейсом;
  • либо поднять до уровня полноценного компонента.

Антипаттерн: “доставка не доведена”

Признаки

  • код существует, но среда его не видит;
  • модуль доступен только после ручной магии;
  • часть живёт в git, часть в БД, часть “должна сама подцепиться”.

Что делать

  • определить единый delivery model;
  • сделать загрузку модулей воспроизводимой и проверяемой;
  • перестать рассчитывать на “оно, наверное, сработает”.

Merge-checklist для платформенного кода

Перед merge каждый новый платформенный модуль должен пройти проверку:

  1. Есть ли короткий spec?
  2. Понятна ли одна причина к изменению?
  3. Есть ли явный контракт входов и выходов?
  4. Нет ли скрытых зависимостей или жёсткого хардкода?
  5. Не смешаны ли несколько контекстов в одном модуле?
  6. Это системный примитив или проектный helper?
  7. Не развиваем ли мы legacy-комбайн вместо нового слоя?
  8. Есть ли docs, changelog и stage-сценарий проверки?

Итоговая позиция

Платформа Metabot развивается не через случайные удобные утилиты, а через осмысленные, ограниченные, документированные и повторно используемые компоненты.

Наша цель — не просто ускорить написание кода, а построить среду, в которой:

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

Каждая новая доработка должна отвечать не только на вопрос “как это сделать”, но и на вопрос “где это должно жить как часть системы”.