chain-of-responsibility-go

📁 progmichaelkibenko/top-coder-agent-skills 📅 1 day ago
1
总安装量
1
周安装量
#54365
全站排名
安装命令
npx skills add https://github.com/progmichaelkibenko/top-coder-agent-skills --skill chain-of-responsibility-go

Agent 安装分布

amp 1
opencode 1
kimi-cli 1
github-copilot 1
claude-code 1

Skill 文档

Chain of Responsibility (Go)

Why: Chain of Responsibility lets you pass a request (or context) along a chain of handlers. Each handler decides whether to process it and pass to the next, or short-circuit. You avoid one big function with all steps and keep each step in its own type (Refactoring.Guru).

Hard constraints: Handlers share a single interface (e.g. Handle(ctx, request)). Each handler holds a reference to the next; the client composes the chain. A handler either processes and passes, or passes without processing.


When to use

  • Validation: Multi-rule validation (required → format → range) where you want to add or reorder rules without editing a single validator.
  • Any sequential pipeline: Processing steps, transformation chains, or multi-step checks where order matters and each step can process and pass (or stop).
  • You want to decouple the sender from concrete handlers and add or reorder steps without changing existing code (Single Responsibility; Open/Closed).

Structure

Role Responsibility
Handler (interface) Declares Handle(ctx, request) (and optionally a setter for next). All concrete handlers implement this.
Base handler (optional) Struct that holds Next; default Handle() forwards to Next if non-nil. Reduces boilerplate.
Concrete handlers Implement Handle(). Process the request (e.g. add errors, transform, check); call h.Next.Handle(ctx, request) or return.
Client Builds the chain (e.g. a.SetNext(b); b.SetNext(c)) and invokes the first handler with the initial request.

A request/context struct is passed through the chain; handlers read it, optionally mutate it, and pass it along (e.g. for validation: Value, FieldName, Errors).


Code contrast (validation example)

Validation is a common use; the same structure applies to any chain. Below: validation.

❌ ANTI-PATTERN: One function with all rules

// One function; every new rule forces edits.
func ValidateOrderInput(data *OrderInput) []ValidationError {
    var errs []ValidationError
    if data.Email == "" {
        errs = append(errs, ValidationError{Field: "email", Message: "email is required"})
    } else if !emailRegex.MatchString(data.Email) {
        errs = append(errs, ValidationError{Field: "email", Message: "invalid email"})
    }
    if data.Amount <= 0 || data.Amount > 10000 {
        errs = append(errs, ValidationError{Field: "amount", Message: "amount must be 1-10000"})
    }
    return errs
}

Problems: order and logic are hardcoded; adding/removing a rule touches this function; rules are hard to test in isolation; violates Open/Closed.

✅ TOP-CODER PATTERN: Validator interface + base + concrete validators + client-built chain

Validator interface and context:

// chain/validator.go
package chain

type ValidationError struct {
    Field   string
    Message string
}

type Context struct {
    Value     any
    FieldName string
    Errors    *[]ValidationError
}

type Validator interface {
    Validate(ctx *Context)
    SetNext(Validator)
}

type BaseValidator struct {
    Next Validator
}

func (b *BaseValidator) SetNext(v Validator) {
    b.Next = v
}

func (b *BaseValidator) Validate(ctx *Context) {
    if b.Next != nil {
        b.Next.Validate(ctx)
    }
}

Concrete validators (embed base, override Validate):

// chain/required.go
type RequiredValidator struct {
    chain.BaseValidator
}

func (v *RequiredValidator) Validate(ctx *chain.Context) {
    if ctx.Value == nil || strings.TrimSpace(fmt.Sprint(ctx.Value)) == "" {
        *ctx.Errors = append(*ctx.Errors, chain.ValidationError{
            Field: ctx.FieldName, Message: ctx.FieldName + " is required",
        })
    }
    v.BaseValidator.Validate(ctx)
}

// chain/email_format.go
var emailRegex = regexp.MustCompile(`^[^@]+@[^@]+\.\w+$`)

type EmailFormatValidator struct {
    chain.BaseValidator
}

func (v *EmailFormatValidator) Validate(ctx *chain.Context) {
    if ctx.Value != nil && ctx.Value != "" && !emailRegex.MatchString(fmt.Sprint(ctx.Value)) {
        *ctx.Errors = append(*ctx.Errors, chain.ValidationError{
            Field: ctx.FieldName, Message: "invalid email format",
        })
    }
    v.BaseValidator.Validate(ctx)
}

// chain/range.go
type RangeValidator struct {
    Min, Max float64
    chain.BaseValidator
}

func (v *RangeValidator) Validate(ctx *chain.Context) {
    n, ok := toFloat(ctx.Value)
    if ctx.Value != nil && (!ok || n < v.Min || n > v.Max) {
        *ctx.Errors = append(*ctx.Errors, chain.ValidationError{
            Field: ctx.FieldName, Message: fmt.Sprintf("must be between %v and %v", v.Min, v.Max),
        })
    }
    v.BaseValidator.Validate(ctx)
}

Client builds one chain per field and runs it:

// service/order.go
emailChain := &chain.RequiredValidator{}
emailChain.SetNext(&chain.EmailFormatValidator{})

amountChain := &chain.RequiredValidator{}
amountChain.SetNext(&chain.RangeValidator{Min: 1, Max: 10000})

func (s *Service) ValidateOrderInput(data *OrderInput) []chain.ValidationError {
    var errs []chain.ValidationError
    ctx := &chain.Context{Errors: &errs}
    ctx.Value, ctx.FieldName = data.Email, "email"
    emailChain.Validate(ctx)
    ctx.Value, ctx.FieldName = data.Amount, "amount"
    amountChain.Validate(ctx)
    return errs
}

Benefits: add or reorder validators by wiring the chain; each validator is a single type, easy to unit test.


Go notes

  • Context struct: Use a shared Context with a pointer to Errors so all validators append to the same slice. For fail-fast, validators can skip calling v.BaseValidator.Validate(ctx) when they add an error.
  • Accept interfaces, return structs: The client depends on the Validator interface; concrete types implement it.
  • Packages: One file per validator when logic is non-trivial (e.g. chain/required.go); keep the interface and base in chain/validator.go.
  • No overkill: For one or two fixed steps, a simple function may be enough; use CoR when you have many steps or dynamic composition.
  • General chains: Same pattern works for non-validation pipelines (e.g. data transformation, enrichment, multi-step processing)—use a request struct that fits the domain and handlers that process and pass.

Pipeline vs Chain of Responsibility

Feature Pipeline Chain of Responsibility
Execution Fixed, mandatory sequence Conditional; handler decides whether to pass to the next
Flow Linear, no branching Allows flexible termination and branching
Termination Runs to completion (barring errors) Can be terminated early by a handler
Use cases Data processing, parsing, ETL Event handling, approval workflows, validation, message filtering

Use Pipeline when every stage must run in a fixed order (e.g. data transformation: parse → normalize → enrich → serialize). Use CoR when handlers can short-circuit or decide not to pass (e.g. validation, approval chains).


Reference