Ведение журнала, сбор метрик и всё, что не связано с основной функциональностью кода, не должно в нём появляться. Вместо этого нужно определить точки трассировки кода, которые могут быть инструментованы пользователем.
Проблема
Пусть у нас
есть пакет под названием lib
и структура lib.Client
, которая пингует
своё основное соединение каждый раз, когда делает запрос.
Что если нам нужно создать
несколько логов до и после пинга? Один из способов состоит в том, чтобы встроить в Client
логгер (или интерфейс с логгером) :
Обычно это необходимо,
когда мы добавляем расчёт метрик в Client
:
Продолжив добавлять методы инструментирования в Client
, мы вскоре поймём, что большая
часть кода связана с инструментированием, а не с основным функционалом
клиента, который был всего лишь одной строкой с вызовом doPing()
.
Количество некогерентных строк кода, то есть не связанных с основной деятельностью нашего клиента – это только первая проблема такого подхода.
Что если во время работы программы вы поймёте, что метрику необходимо переименовать? Или нужно использовать другую библиотеку для логирования? При описанном подходе нужно будет перейти к реализации клиента, а также других подобных компонентов и изменить её.
Это означает, что придется менять код каждый раз, когда изменяется что-то не связанное с основной функциональностью компонента, а такие действия нарушают принцип SRP.
Все эти моменты всплывают из-за ошибки в проектировании: мы не должны переживать о том, какие методы инструментирования пользователь захочет использовать с нашим компонентом.
Решение
Правильный способ всё исправить – определить точки трассировки (hooks), которые пользователь вашего кода сможет инициализировать с помощью некоторой функции (probe) во время рантайма.
Это, конечно, добавит дополнительные строки кода, но даст пользователям гибкость для измерения времени выполнения компонента с помощью любого подходящего метода.
Такой подход используется, например, пакетом httptrace из стандартной библиотеки.
Давайте представим
почти ту же механику, но с одним изменением. Вместо того чтобы предоставлять хуки
OnPingStart()
и OnPingDone()
, введем один OnPing()
, который будет вызван
непосредственно перед ping
и вернет обратный вызов, вызванный после ping
.
Таким образом, мы можем хранить некоторые переменные в замыкании, чтобы
получить к ним доступ после завершения ping
(например, для расчета его задержки).
Давайте взглянем, как
изменится Client
при таком подходе:
Выглядит аккуратно, но
только до тех пор, пока мы не поймём, что как при выполнении OnPing
, так и при обратном вызове он может вернуться с нулевым значением:
Теперь и с гибкостью всё хорошо, и в норме принцип SRP, но код перестал быть простым.
Прежде чем заняться упрощением, рассмотрим ещё одну проблему.
Создание хуков
Как пользователь сможет
создать несколько prob-ов? Упомянутый ранее пакет
httptrace
включает в себя ClientTrace.compose()
, который объединяет две
структуры трассировки в третью. Таким образом, каждая probe-функция из результирующей трассировки вызовет
соответствующие prob-ы предыдущих трассировок.
Поэтому сначала попробуем
сделать то же самое вручную и без рефлексии. Для этого мы перемещаем OnPing
из
клиента в отдельную структуру ClientTrace
:
А объединение двух трассировок будет выглядеть следующим образом:
Столько кода для одного хука? Пока двинемся вперед, вернёмся к этому позже. Теперь пользователь может самостоятельно настроить или изменить любой метод инструментирования:
Контекстная трассировка
Ещё одна штука, которую
мы можем предоставить юзерам – контекстная трассировка. Это примерно то же
самое, что и в пакете httptrace
– возможность связывать хуки с context.Context
,
передаваемым в Client.Request()
.
Похоже, что теперь можно переносить все данные практики в пользовательский компонент.
Остаётся одно «но»: писать весь этот код для каждой структуры – рутина. Конечно, можно написать vim-макросы, но давайте рассмотрим альтернативы.
Объединение хуков, проверка на ноль и контекстно-зависимые функции – шаблонны. Значит, мы можем генерировать для них код Go без макросов или рефлексии.
Инструмент gtrace
Инструмент
командной строки gtrace генерирует код Go для описанной выше трассировки. Он
предлагает структуры с полями хуков (помеченные ниже / / gtrace:gen
), а
также генерирует вспомогательные функции вокруг них, так что мы можем
объединять данные структуры и вызывать хуки без каких-либо проверок. Он
также умеет создавать контекстно-зависимых хелперы для вызова связанных с
контекстом хуков.
Пример сгенерированного кода находится на официальном гитхабе.
Мы можем отбросить весь «лишний» код, написанный выше, и оставить только следующий блок:
После запуска go
generate
можно использовать сгенерированные неэкспортированные версии хуков
трассировки, которые мы определили в ClientTrace
.
Вот и всё! Модуль gtrace
позаботится о шаблонном коде и позволит сосредоточиться на точках
трассировки, которые могут быть инструментированы пользователями.
Заключение
В данной статье мы рассмотрели важную проблему инструментирования в программах на языке программирования Go. Вся сложность заключается в гибкости и переносимости кода – обычные подходы к решению задачи приводят к нагромождению и ухудшению читабельности. Подход, рассмотренный здесь, вы можете смело брать на вооружение в вашем следующем проекте.
Комментарии