В этой статье я объясню концепцию шаблона проектирования цепочки ответственности, цели, плюсы и минусы, сценарии и способы реализации, а также предоставлю два экземпляра и модульные тесты.

Нажмите, чтобы стать средним участником и читать неограниченное количество историй!

Концепция

Шаблон цепочки ответственности — это поведенческий шаблон проектирования, который позволяет отделить отправителя запроса от его получателей, позволяя нескольким объектам обрабатывать запрос. Шаблон создает цепочку объектов-получателей и передает запрос по цепочке до тех пор, пока объект не обработает его. Это означает, что этот шаблон проектирования позволяет нескольким объектам обрабатывать запрос, при этом каждый объект имеет возможность либо обработать запрос, либо передать его следующему объекту в цепочке.

Цели

Целями схемы цепочки ответственности являются:

  1. Разделение отправителя и получателя запроса.
  2. Разрешение нескольким обработчикам обрабатывать запрос без указания того, какой обработчик должен это делать.
  3. Разрешение динамического добавления или удаления обработчиков в цепочке.
  4. Снижение сложности обработки запросов путем их разбиения на более мелкие и более управляемые части.

За и против

Преимущества использования шаблона цепочки ответственности:

  1. Повышенная гибкость при назначении ответственности объектам.
  2. Повышенная расширяемость, поскольку можно легко добавлять новые обработчики.
  3. Упрощенный код, так как каждому обработчику нужно сосредоточиться только на своей конкретной задаче.

Недостатки использования шаблона цепочки ответственности:

  1. Время обработки может быть увеличено, поскольку запрос проходит через несколько обработчиков.
  2. Отладка и обслуживание могут быть более сложными из-за распределенного характера обработчиков.

Сценарии

Шаблон цепочки ответственности полезен в следующих сценариях:

1. Запросить подтверждение

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

2. ПО промежуточного слоя

В веб-разработке промежуточное ПО часто обрабатывает запросы до того, как они достигают фактической логики приложения. ПО промежуточного слоя может выполнять различные задачи, включая аутентификацию, ведение журнала запросов, кэширование или ограничение скорости. Шаблон цепочки ответственности можно использовать для создания ряда обработчиков ПО промежуточного слоя, которые обрабатывают запрос в определенном порядке.

3. Обработка событий

В системах, управляемых событиями, за обработку одного события может отвечать несколько обработчиков. У каждого обработчика могут быть свои собственные задачи, такие как ведение журнала, аналитика или обновление состояния приложения. Шаблон цепочки ответственности позволяет организовать эти обработчики таким образом, чтобы они обрабатывали событие в определенном порядке, и чтобы один обработчик мог решить, продолжить обработку или остановить цепочку.

4. Обработка ошибок

В приложении для разных типов ошибок могут потребоваться разные стратегии обработки. Например, некоторые ошибки могут быть устранены, в то время как для других может потребоваться полное завершение работы. Шаблон цепочки ответственности можно использовать для создания серии обработчиков ошибок, предназначенных для определенных типов ошибок или уровней серьезности. Когда возникает ошибка, цепочка обрабатывает ошибку до тех пор, пока обработчик не сможет ее обработать или цепочка не будет исчерпана.

5. Обработка документов

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

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

Как реализовать

Шаги по реализации шаблона цепочки ответственности в Go:

  1. Определите интерфейс обработчика.
  2. Создайте конкретные реализации обработчика.
  3. Реализуйте дополнительные конкретные обработчики по мере необходимости.
  4. Создайте цепочку.
  5. Используйте цепь.

Первый случай

Сначала я реализую простой пример. Приведенный ниже код является содержимым файла chain.go.

package simple

type Handler interface {
   SetNext(Handler)
   Handle(*Request) string
}

type Request struct {
   Type  string
   Value int
}

type ConcreteHandlerA struct {
   Next Handler
}

func (h *ConcreteHandlerA) SetNext(next Handler) {
   h.Next = next
}

func (h *ConcreteHandlerA) Handle(req *Request) (resp string) {
   if req.Value >= 0 && req.Value < 10 {
      return "ConcreteHandlerA handled the request"
   }
   if h.Next != nil {
      return h.Next.Handle(req)
   }
   return
}

type ConcreteHandlerB struct {
   Next Handler
}

func (h *ConcreteHandlerB) SetNext(next Handler) {
   h.Next = next
}

func (h *ConcreteHandlerB) Handle(req *Request) (resp string) {
   if req.Value >= 10 && req.Value < 20 {
      return "ConcreteHandlerB handled the request"
   }
   if h.Next != nil {
      return h.Next.Handle(req)
   }
   return
}

func createHandlerChain() Handler {
   handlerA := &ConcreteHandlerA{}
   handlerB := &ConcreteHandlerB{}

   handlerA.SetNext(handlerB)
   return handlerA
}

В приведенном выше коде показано, как реализовать шаги.

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

Второй шаг — реализовать интерфейс Handler в конкретных типах обработчиков, каждый из которых способен обрабатывать определенные запросы.

Третий шаг — структуры ConcreteHandlerA и ConcreteHandlerB реализуют интерфейс Handler и представляют конкретные обработчики в цепочке.

Четвертый шаг — функция createHandlerChain().

Пятый шаг заключается в следующем содержимом кода, принадлежащем файлу chain_test.go.

package simple

import (
   "testing"

   "github.com/go-playground/assert/v2"
)

func TestHandlerA(t *testing.T) {
   handler := createHandlerChain()

   // create a Request structure and pass it to the first handler in the chain (h1) using the Handle method.
   req := &Request{
      Value: 5,
   }
   assert.Equal(t, "ConcreteHandlerA handled the request", handler.Handle(req))
} 

func TestHandlerB(t *testing.T) {
   handler := createHandlerChain()

   // create a Request structure and pass it to the first handler in the chain (h1) using the Handle method.
   req := &Request{
      Value: 15,
   }

   assert.Equal(t, "ConcreteHandlerB handled the request", handler.Handle(req))
}

В приведенном выше коде я реализую два теста. Пятый шаг — h1.Handle(req) в обоих случаях, он использует цепочку.

Скриншот результатов теста ниже.

Второй экземпляр

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

Приведенный ниже код является содержимым файла logger.go.

package logger

import (
   "log"
)

type LogLevel int

const (
   DEBUG LogLevel = iota
   INFO
   ERROR
)

type Handler interface {
   SetNext(Handler)
   HandleLog(level LogLevel, message string)
}

type DebugHandler struct {
   next   Handler
   logger *log.Logger
}

func (h *DebugHandler) SetNext(next Handler) {
   h.next = next
}

func (h *DebugHandler) HandleLog(level LogLevel, message string) {
   if level == DEBUG {
      h.logger.Println("[DEBUG]:", message)
   }

   if h.next != nil {
      h.next.HandleLog(level, message)
   }
}

type InfoHandler struct {
   next   Handler
   logger *log.Logger
}

func (h *InfoHandler) SetNext(next Handler) {
   h.next = next
}

func (h *InfoHandler) HandleLog(level LogLevel, message string) {
   if level == INFO {
      h.logger.Println("[INFO]:", message)
   }

   if h.next != nil {
      h.next.HandleLog(level, message)
   }
}

type ErrorHandler struct {
   next   Handler
   logger *log.Logger
}

func (h *ErrorHandler) SetNext(next Handler) {
   h.next = next
}

func (h *ErrorHandler) HandleLog(level LogLevel, message string) {
   if level == ERROR {
      h.logger.Println("[ERROR]:", message)
   }

   if h.next != nil {
      h.next.HandleLog(level, message)
   }
}

func createHandlerChain(debugLogger, infoLogger, errorLogger *log.Logger) Handler {
   debugHandler := &DebugHandler{logger: debugLogger}
   infoHandler := &InfoHandler{logger: infoLogger}
   errorHandler := &ErrorHandler{logger: errorLogger}

   debugHandler.SetNext(infoHandler)
   infoHandler.SetNext(errorHandler)

   return debugHandler
}

В приведенном выше коде сначала определите интерфейсы LogLevel и Handler. После этого создайте конкретные реализации обработчиков для каждого уровня журнала. DebugHandler, InfoHandler и ErrorHandler — это три конкретных обработчика. Каждый конкретный обработчик встраивает регистратор, который будет использоваться для регистрации сообщений. У каждого обработчика есть метод SetNext, который устанавливает следующий обработчик в цепочке. Метод HandleLog для каждого обработчика проверяет, соответствует ли уровень журнала его ответственности. Если оно совпадает, обработчик регистрирует сообщение; если нет, он перенаправляет сообщение следующему обработчику в цепочке. В конце создайте функцию createHandlerChain для создания и связывания цепочки обработчиков. Эта функция принимает регистраторы для каждого обработчика и возвращает первый обработчик в цепочке.

Использование цепочки указано в содержимом файла logger_test.go. Ниже приведен файл logger_test.go.

package logger

import (
   "bytes"
   "log"
   "testing"
)

func TestHandlerChain(t *testing.T) {
   tests := []struct {
      name    string
      level   LogLevel
      message string
      want    string
   }{
      {"DebugMessage", DEBUG, "Debug Test", "[DEBUG]: Debug Test\n"},
      {"InfoMessage", INFO, "Info Test", "[INFO]: Info Test\n"},
      {"ErrorMessage", ERROR, "Error Test", "[ERROR]: Error Test\n"},
   }

   for _, tt := range tests {
      t.Run(tt.name, func(t *testing.T) {
         var buf bytes.Buffer
         logger := log.New(&buf, "", 0)

         handlerChain := createHandlerChain(logger, logger, logger)
         handlerChain.HandleLog(tt.level, tt.message)

         got := buf.String()
         if got != tt.want {
            t.Errorf("HandleLog() = %v, want %v", got, tt.want)
         }
      })
   }
}

Тесты предназначены для того, чтобы гарантировать, что каждый обработчик в цепочке регистрирует сообщения с правильным уровнем журнала. Во-первых, я определяю серию тестов с разными уровнями логирования и ожидаемыми результатами. Для каждого теста я запускаю подтест, который создает bytes.Buffer и регистратор, который записывает в этот буфер. Затем я создаю цепочку обработчиков с пользовательским регистратором и вызываю HandleLog с указанным уровнем журнала и сообщением. После запуска метода HandleLog я проверяю содержимое буфера на соответствие ожидаемому результату. Если выходные данные не соответствуют ожидаемому значению, тест завершается неудачно и сообщает об ошибке.

В примере я создал простую систему ведения журнала с различными уровнями ведения журнала, используя шаблон цепочки ответственности. Каждый уровень журнала представлен конкретным обработчиком, и цепочка строится путем связывания этих обработчиков вместе. Когда сообщение журнала передается первому обработчику в цепочке, оно обрабатывается и печатается, если уровень журнала совпадает, в противном случае оно передается следующему обработчику в цепочке. И набор тестов гарантирует, что каждый обработчик правильно регистрирует сообщения с соответствующим уровнем журнала и что цепочка ответственности работает как задумано.

Скриншот результатов теста ниже.

Заключение

Таким образом, паттерн «Цепь ответственности» — это мощный инструмент для обработки запросов в среде объектно-ориентированного программирования, но его следует использовать с осторожностью, чтобы максимизировать его преимущества и свести к минимуму его недостатки.

Шаблон Chain of Responsibility открывает двери промежуточному программному обеспечению любого типа и плагиноподобным библиотекам для улучшения функциональности некоторых частей. Многие проекты с открытым исходным кодом используют цепочку ответственности для обработки HTTP-запросов и ответов для извлечения информации от конечного пользователя или проверки деталей аутентификации.

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

Чтобы просмотреть шаблоны креативного дизайна в Golang, нажмите здесь.

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

Спасибо, что читаете. Если вам понравилась моя статья, хлопайте в ладоши и подписывайтесь на меня. Я с удовольствием отвечу на все ваши вопросы, если вы спросите меня в комментарии. Нажмите на следующую ссылку, чтобы стать средним участником.

Нажмите, чтобы стать средним участником и читать неограниченное количество историй!