Chain of Responsibility (Цепочка обязанностей)
Проблема
Запрос должен быть обработан одним из нескольких обработчиков, но заранее неизвестно каким. Каждый обработчик решает: обработать запрос или передать дальше по цепочке. Это позволяет настраивать цепочку динамически.
В Go этот паттерн повсюду: HTTP middleware -- классический Chain of Responsibility.
Диаграмма
Request --> Auth --> RateLimit --> Validation --> Handler
| | |
401 429 400 (если не прошёл)
|
next(request) (если прошёл -- передать дальше)
Реализация: Цепочка обработчиков
package chain
import "fmt"
// Request represents a generic request.
type Request struct {
UserID string
Action string
Resource string
IPAddress string
Token string
Body map[string]any
}
// Response is the handler result.
type Response struct {
Allowed bool
Reason string
}
// Handler processes a request or passes it to the next handler.
type Handler interface {
Handle(req Request) Response
SetNext(handler Handler) Handler
}
// BaseHandler provides default chaining behavior.
type BaseHandler struct {
next Handler
}
func (h *BaseHandler) SetNext(handler Handler) Handler {
h.next = handler
return handler
}
func (h *BaseHandler) HandleNext(req Request) Response {
if h.next != nil {
return h.next.Handle(req)
}
return Response{Allowed: true, Reason: "passed all checks"}
}
Конкретные обработчики
// AuthHandler checks if the request has a valid token.
type AuthHandler struct {
BaseHandler
validTokens map[string]string // token -> userID
}
func NewAuthHandler(tokens map[string]string) *AuthHandler {
return &AuthHandler{validTokens: tokens}
}
func (h *AuthHandler) Handle(req Request) Response {
if req.Token == "" {
return Response{Allowed: false, Reason: "missing auth token"}
}
if _, ok := h.validTokens[req.Token]; !ok {
return Response{Allowed: false, Reason: "invalid auth token"}
}
return h.HandleNext(req)
}
// RateLimitHandler limits requests per IP.
type RateLimitHandler struct {
BaseHandler
counts map[string]int
limit int
}
func NewRateLimitHandler(limit int) *RateLimitHandler {
return &RateLimitHandler{
counts: make(map[string]int),
limit: limit,
}
}
func (h *RateLimitHandler) Handle(req Request) Response {
h.counts[req.IPAddress]++
if h.counts[req.IPAddress] > h.limit {
return Response{
Allowed: false,
Reason: fmt.Sprintf("rate limit exceeded for IP %s", req.IPAddress),
}
}
return h.HandleNext(req)
}
// PermissionHandler checks if user can perform the action.
type PermissionHandler struct {
BaseHandler
permissions map[string][]string // userID -> allowed actions
}
func NewPermissionHandler(perms map[string][]string) *PermissionHandler {
return &PermissionHandler{permissions: perms}
}
func (h *PermissionHandler) Handle(req Request) Response {
actions, ok := h.permissions[req.UserID]
if !ok {
return Response{Allowed: false, Reason: "no permissions defined"}
}
for _, a := range actions {
if a == req.Action || a == "*" {
return h.HandleNext(req)
}
}
return Response{
Allowed: false,
Reason: fmt.Sprintf("user %s cannot %s", req.UserID, req.Action),
}
}
Сборка цепочки
func main() {
auth := NewAuthHandler(map[string]string{
"token-123": "alice",
"token-456": "bob",
})
rateLimit := NewRateLimitHandler(100)
perms := NewPermissionHandler(map[string][]string{
"alice": {"read", "write", "delete"},
"bob": {"read"},
})
// Build the chain
auth.SetNext(rateLimit).SetNext(perms)
// Test requests
req := Request{
UserID: "alice",
Action: "write",
Token: "token-123",
IPAddress: "192.168.1.1",
}
resp := auth.Handle(req)
fmt.Printf("Allowed: %v, Reason: %s\n", resp.Allowed, resp.Reason)
}
Идиоматичный Go: middleware функции
В Go более распространён функциональный подход без SetNext():
package middleware
import (
"log/slog"
"net/http"
"time"
)
// Middleware is a function that wraps an http.Handler.
type Middleware func(http.Handler) http.Handler
// Chain applies middlewares in order.
// First middleware is the outermost (executed first).
func Chain(h http.Handler, mws ...Middleware) http.Handler {
for i := len(mws) - 1; i >= 0; i-- {
h = mws[i](h)
}
return h
}
// RequestID adds a request ID to each request.
func RequestID(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
id := fmt.Sprintf("req-%d", time.Now().UnixNano())
r.Header.Set("X-Request-ID", id)
w.Header().Set("X-Request-ID", id)
next.ServeHTTP(w, r)
})
}
// Logger logs each request.
func Logger(logger *slog.Logger) Middleware {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
next.ServeHTTP(w, r)
logger.Info("request",
"method", r.Method,
"path", r.URL.Path,
"duration", time.Since(start),
"request_id", r.Header.Get("X-Request-ID"),
)
})
}
}
// Auth checks for authorization header.
func Auth(secret string) Middleware {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
token := r.Header.Get("Authorization")
if token != "Bearer "+secret {
http.Error(w, "Unauthorized", http.StatusUnauthorized)
return // stop the chain
}
next.ServeHTTP(w, r)
})
}
}
Использование:
func main() {
mux := http.NewServeMux()
mux.HandleFunc("GET /api/data", handleData)
handler := middleware.Chain(mux,
middleware.RequestID,
middleware.Logger(slog.Default()),
middleware.Auth("my-secret"),
)
http.ListenAndServe(":8080", handler)
}
Конвейер валидации
package validation
import "fmt"
// Validator validates a value and returns an error if invalid.
type Validator func(value any) error
// Chain runs validators in sequence, stopping at first error.
func Chain(validators ...Validator) Validator {
return func(value any) error {
for _, v := range validators {
if err := v(value); err != nil {
return err
}
}
return nil
}
}
// NotEmpty checks that string is not empty.
func NotEmpty(field string) Validator {
return func(value any) error {
s, ok := value.(string)
if !ok || s == "" {
return fmt.Errorf("%s is required", field)
}
return nil
}
}
// MinLength checks minimum string length.
func MinLength(field string, min int) Validator {
return func(value any) error {
s, _ := value.(string)
if len(s) < min {
return fmt.Errorf("%s must be at least %d characters", field, min)
}
return nil
}
}
Когда использовать
Используйте, когда:
- Несколько обработчиков для одного запроса
- Порядок и состав обработчиков настраивается динамически
- HTTP middleware pipeline
- Конвейеры валидации
Не используйте, когда:
- Обработчик всегда один (просто вызовите его)
- Порядок фиксирован и прост (обычная последовательность вызовов)
Сравнение с похожими паттернами
| Паттерн | Отличие |
|---|---|
| Decorator | Все декораторы выполняются; в CoR цепочка может прерваться |
| Command | Инкапсулирует действие; CoR передаёт запрос по цепочке |
| Mediator | Централизованная координация; CoR -- последовательная |