> Какие паттерны проектирования применить для процессора с выбором стратегии обработки по типу события (Go)

Уровень: senior · Роль: backend · Язык: Go · Категория: Технические вопросы

Компании: Wildberries

Стек: Go

> Пример ответа

Короткий ответ

Для процессора с выбором стратегии обработки по типу события в Go оптимально сочетание паттернов Strategy и Registry (реализуемого через map[string]HandlerFunc). Strategy инкапсулирует алгоритмы обработки, Registry обеспечивает динамический выбор по типу события. Дополнительно можно использовать Factory Method для создания обработчиков и Pipeline для цепочек обработки. В Go это естественно выражается через интерфейсы и функции первого класса, без избыточной иерархии классов.

Подробное объяснение

Классическая постановка: есть поток событий разных типов, каждому типу соответствует своя логика обработки. Прямой switch/case - антипаттерн при росте числа типов: он нарушает open/closed principle и усложняет тестирование.

Strategy - базовый паттерн: определяем интерфейс Handler с методом Handle(ctx, event), каждая стратегия - отдельная структура или функция. Это даёт изоляцию логики, лёгкое добавление новых типов без изменения существующего кода.

Registry - дополнение к Strategy: храним соответствие eventType -> Handler в map. Выбор стратегии становится O(1) по типу. В Go это идиоматично: map[string]HandlerFunc - стандартный подход для роутеров (например, в net/http).

Factory Method - если создание обработчика требует зависимостей (БД, клиенты API), фабрика централизует их внедрение. В Go часто заменяется простой функцией-конструктором.

Pipeline - если обработка события - последовательность шагов (валидация, обогащение, сохранение), можно выстроить цепочку middleware. Но это отдельный сценарий, не обязательный для базового выбора стратегии.

Trade-off: Registry через map - проще и быстрее, но требует ручной регистрации. Альтернатива - рефлексия или кодогенерация, но они усложняют поддержку. Для senior-уровня важно показать понимание, когда хватает простого map, а когда нужен полноценный DI-контейнер.

На практике

В реальном Go-коде паттерн Strategy+Registry выглядит так:

  1. Определяем интерфейс обработчика - часто это просто func(ctx context.Context, event Event) error.
  2. Создаём registry - структуру с map[string]HandlerFunc и методами Register и Get.
  3. Регистрируем обработчики в init() или в явной функции RegisterHandlers(registry).
  4. В процессоре получаем обработчик по типу события, обрабатываем ошибку "неизвестный тип".

Важные детали:

  • Регистрация должна быть потокобезопасной, если происходит в рантайме (обычно используется sync.RWMutex или регистрация только при старте).
  • Обработка отсутствующего типа - отдельная стратегия (default handler), а не паника.
  • Для тестирования удобно инжектить registry с моками.

Пример кода

GO
type Event struct {
Type string
Data []byte
}
type HandlerFunc func(ctx context.Context, ev Event) error
type Registry struct {
mu sync.RWMutex
handlers map[string]HandlerFunc
}
func NewRegistry() *Registry {
return &Registry{handlers: make(map[string]HandlerFunc)}
}
func (r *Registry) Register(eventType string, h HandlerFunc) {
r.mu.Lock()
defer r.mu.Unlock()
r.handlers[eventType] = h
}
func (r *Registry) Get(eventType string) (HandlerFunc, bool) {
r.mu.RLock()
defer r.mu.RUnlock()
h, ok := r.handlers[eventType]
return h, ok
}
type Processor struct {
registry *Registry
}
func (p *Processor) Process(ctx context.Context, ev Event) error {
handler, ok := p.registry.Get(ev.Type)
if !ok {
return fmt.Errorf("unknown event type: %s", ev.Type)
}
return handler(ctx, ev)
}
// регистрация
func RegisterHandlers(r *Registry, db *sql.DB) {
r.Register("user.created", func(ctx context.Context, ev Event) error {
// обработка создания пользователя
return nil
})
r.Register("order.paid", func(ctx context.Context, ev Event) error {
// обработка оплаты заказа
return nil
})
}

Как отвечать на собеседовании

Начни с краткого ответа: Strategy + Registry. Затем поясни, почему именно эти паттерны: Strategy даёт полиморфизм обработки, Registry - динамический выбор по типу. Упомяни, что в Go вместо классической иерархии классов используем функции и map - это идиоматично.

Покажи понимание trade-off: map - просто и быстро, но требует ручной регистрации; если типов сотни - рассмотри кодогенерацию или таблицы с метаданными. Упомяни, что switch/case допустим для 2-3 типов, но не масштабируется.

Если спросят про альтернативы - скажи про Visitor (если структура событий фиксирована, а операции меняются) и про Chain of Responsibility (если обработчики могут передавать событие дальше). Но подчеркни, что для типичного event processor Strategy+Registry - оптимальный выбор.

Заверши примером кода - покажи registry с map и обработку неизвестного типа. Это демонстрирует практический опыт.

Что проверяет интервьюер

  • Понимание базовых паттернов GoF и умение адаптировать их к идиоматике Go.
  • Способность видеть границы применимости: когда map достаточно, когда нужен DI.
  • Умение проектировать расширяемую архитектуру без overengineering.
  • Понимание конкурентности: потокобезопасность registry, обработка событий параллельно.
  • Практические навыки: обработка ошибок, тестируемость, чистота API.

Типичные ошибки

  • Предлагать классическую иерархию классов с интерфейсами для каждого обработчика - в Go это избыточно, достаточно функций.
  • Забывать про потокобезопасность registry при регистрации в рантайме.
  • Использовать рефлексию для выбора обработчика - это медленно и нечитаемо.
  • Не обрабатывать случай неизвестного типа - паника или молчаливый пропуск события.
  • Смешивать Strategy с цепочкой обязанностей: если обработчик может делегировать дальше - это уже другой паттерн, не усложняй базовое решение.
  • Регистрировать обработчики в init() без явного порядка - лучше явная функция инициализации.

> ГОТОВЫ К СЛЕДУЮЩЕМУ СОБЕСЕДОВАНИЮ?

Запустите тренировочную сессию с ИИ и получите детальную обратную связь, чтобы увереннее проходить реальные интервью