chain-of-responsibility-go
npx skills add https://github.com/progmichaelkibenko/top-coder-agent-skills --skill chain-of-responsibility-go
Agent 安装分布
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
Contextwith a pointer toErrorsso all validators append to the same slice. For fail-fast, validators can skip callingv.BaseValidator.Validate(ctx)when they add an error. - Accept interfaces, return structs: The client depends on the
Validatorinterface; concrete types implement it. - Packages: One file per validator when logic is non-trivial (e.g.
chain/required.go); keep the interface and base inchain/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
- Chain of Responsibility â Refactoring.Guru: intent, problem/solution, structure, applicability, pros/cons, relations with Command/Decorator/Composite.