Отложенный запуск должен быть отдельным инструментом
Симптом
Каждое новое требование к отложенным действиям заставляло добавлять ещё одну специализированную реализацию. Код постепенно разрастался, а один и тот же механизм планирования начинал жить внутри разных инструментов.
Контекст
Сначала отложенную отправку сообщений реализовали внутри инструмента для Telegram. Затем такой же подход применили для VK. Когда понадобилась отложенная отправка email, стало очевидно, что добавление ещё одной копии механизма только увеличит связанность и стоимость изменений.
Корневая причина
Функцию отложенного запуска не отделили от логики самих инструментов. Каждый инструмент одновременно отвечал за две разные задачи:
- выполнить свою предметную операцию;
- решить, когда её нужно выполнить.
Из-за этого поддержка нового канала требовала менять не только его отправку, но и очередной вариант планировщика.
Ошибочное предположение
Отложенный вызов считался частным и редким случаем, который можно реализовать рядом с конкретным инструментом. Пока инструмент был один, такое решение выглядело дешевле отдельного слоя. Повторное требование для другого канала показало, что это не свойство канала, а самостоятельная архитектурная функция.
Обнаружение
Сигналом стало повторение одной и той же реализации: Telegram и VK получили собственные механизмы отложенной отправки, а появление email потребовало бы третьего. Такой рост дублирования можно обнаруживать на этапе архитектурного разбора требований, ещё до реализации нового канала.
Исправление
Я вынес отложенный запуск в отдельный инструмент, который может запланировать вызов другого инструмента. Теперь логика планирования не зависит от канала или предметной операции, а агент может отложить запуск любого поддерживаемого инструмента.
Инструменты отвечают за выполнение своих операций, а отдельный слой - за момент запуска. Это уменьшает дублирование и позволяет добавлять новые каналы без копирования механизма планирования.
Проверка
Минимальный регрессионный сценарий должен проверить один и тот же планировщик с несколькими разными инструментами:
- запланировать вызов инструмента отправки сообщения;
- запланировать вызов инструмента отправки email;
- убедиться, что оба вызова запускаются в заданное время и получают исходные аргументы;
- проверить, что отмена или ошибка одного запланированного вызова не меняет поведение остальных.
В исходной заметке автоматический тест или монитор для этого контракта не зафиксирован. Его стоит добавить, чтобы новое требование не вернуло логику планирования обратно в отдельные инструменты.
Общий вывод
Планирование запуска - отдельная архитектурная ответственность, а не частная особенность конкретной функции. Если один и тот же сценарий появляется у нескольких инструментов, его нужно вынести на уровень общей модели и проверять единым контрактом.