Behavioral Design Patterns
Behavioral patterns deal with object collaboration and responsibility distribution.
Observer Pattern
Define one-to-many dependency between objects.
Good: Observer Pattern
package main
import (
"fmt"
)
type Observer interface {
Update(subject Subject)
}
type Subject interface {
Attach(observer Observer)
Detach(observer Observer)
Notify()
}
type ConcreteSubject struct {
state int
observers []Observer
}
func (cs *ConcreteSubject) Attach(observer Observer) {
cs.observers = append(cs.observers, observer)
}
func (cs *ConcreteSubject) Detach(observer Observer) {
// Remove observer
}
func (cs *ConcreteSubject) Notify() {
for _, observer := range cs.observers {
observer.Update(cs)
}
}
func (cs *ConcreteSubject) SetState(state int) {
cs.state = state
cs.Notify()
}
func (cs *ConcreteSubject) GetState() int {
return cs.state
}
type ConcreteObserver struct {
name string
}
func (co *ConcreteObserver) Update(subject Subject) {
if cs, ok := subject.(*ConcreteSubject); ok {
fmt.Printf("%s: State changed to %d\n", co.name, cs.GetState())
}
}
func main() {
subject := &ConcreteSubject{}
observer1 := &ConcreteObserver{name: "Observer1"}
observer2 := &ConcreteObserver{name: "Observer2"}
subject.Attach(observer1)
subject.Attach(observer2)
subject.SetState(42)
}
Strategy Pattern
Define family of algorithms and make them interchangeable.
Good: Strategy Pattern
package main
import (
"fmt"
"sort"
)
type SortStrategy interface {
Sort([]int)
}
type BubbleSort struct{}
func (bs *BubbleSort) Sort(arr []int) {
fmt.Println("Sorting with bubble sort")
// Bubble sort implementation
}
type QuickSort struct{}
func (qs *QuickSort) Sort(arr []int) {
fmt.Println("Sorting with quick sort")
sort.Ints(arr)
}
type Sorter struct {
strategy SortStrategy
}
func (s *Sorter) SetStrategy(strategy SortStrategy) {
s.strategy = strategy
}
func (s *Sorter) Sort(arr []int) {
s.strategy.Sort(arr)
}
func main() {
sorter := &Sorter{strategy: &BubbleSort{}}
sorter.Sort([]int{3, 1, 2})
sorter.SetStrategy(&QuickSort{})
sorter.Sort([]int{3, 1, 2})
}
Command Pattern
Encapsulate request as object.
Good: Command Pattern
package main
import (
"fmt"
)
type Command interface {
Execute()
Undo()
}
type Light struct {
isOn bool
}
func (l *Light) TurnOn() {
l.isOn = true
fmt.Println("Light is on")
}
func (l *Light) TurnOff() {
l.isOn = false
fmt.Println("Light is off")
}
type TurnOnCommand struct {
light *Light
}
func (toc *TurnOnCommand) Execute() {
toc.light.TurnOn()
}
func (toc *TurnOnCommand) Undo() {
toc.light.TurnOff()
}
type TurnOffCommand struct {
light *Light
}
func (tfc *TurnOffCommand) Execute() {
tfc.light.TurnOff()
}
func (tfc *TurnOffCommand) Undo() {
tfc.light.TurnOn()
}
type RemoteControl struct {
commands []Command
}
func (rc *RemoteControl) Execute(command Command) {
command.Execute()
rc.commands = append(rc.commands, command)
}
func (rc *RemoteControl) Undo() {
if len(rc.commands) > 0 {
last := rc.commands[len(rc.commands)-1]
last.Undo()
rc.commands = rc.commands[:len(rc.commands)-1]
}
}
func main() {
light := &Light{}
remote := &RemoteControl{}
remote.Execute(&TurnOnCommand{light: light})
remote.Execute(&TurnOffCommand{light: light})
remote.Undo()
}
State Pattern
Allow object to change behavior when state changes.
Good: State Pattern
package main
import (
"fmt"
)
type State interface {
Handle(context *Context)
}
type Context struct {
state State
}
func (c *Context) SetState(state State) {
c.state = state
}
func (c *Context) Request() {
c.state.Handle(c)
}
type ConcreteStateA struct{}
func (csa *ConcreteStateA) Handle(context *Context) {
fmt.Println("State A handling request")
context.SetState(&ConcreteStateB{})
}
type ConcreteStateB struct{}
func (csb *ConcreteStateB) Handle(context *Context) {
fmt.Println("State B handling request")
context.SetState(&ConcreteStateA{})
}
func main() {
context := &Context{state: &ConcreteStateA{}}
context.Request()
context.Request()
context.Request()
}
Template Method Pattern
Define skeleton of algorithm in base class.
Good: Template Method Pattern
package main
import (
"fmt"
)
type DataProcessor interface {
ReadData()
ProcessData()
WriteData()
}
type AbstractProcessor struct {
processor DataProcessor
}
func (ap *AbstractProcessor) Execute() {
ap.processor.ReadData()
ap.processor.ProcessData()
ap.processor.WriteData()
}
type CSVProcessor struct{}
func (cp *CSVProcessor) ReadData() {
fmt.Println("Reading CSV data")
}
func (cp *CSVProcessor) ProcessData() {
fmt.Println("Processing CSV data")
}
func (cp *CSVProcessor) WriteData() {
fmt.Println("Writing CSV data")
}
type JSONProcessor struct{}
func (jp *JSONProcessor) ReadData() {
fmt.Println("Reading JSON data")
}
func (jp *JSONProcessor) ProcessData() {
fmt.Println("Processing JSON data")
}
func (jp *JSONProcessor) WriteData() {
fmt.Println("Writing JSON data")
}
func main() {
csvProc := &AbstractProcessor{processor: &CSVProcessor{}}
csvProc.Execute()
jsonProc := &AbstractProcessor{processor: &JSONProcessor{}}
jsonProc.Execute()
}
Iterator Pattern
Access elements sequentially without exposing structure.
Good: Iterator Pattern
package main
import (
"fmt"
)
type Iterator interface {
HasNext() bool
Next() interface{}
}
type Collection interface {
CreateIterator() Iterator
}
type IntCollection struct {
items []int
}
func (ic *IntCollection) CreateIterator() Iterator {
return &IntIterator{collection: ic, index: 0}
}
type IntIterator struct {
collection *IntCollection
index int
}
func (ii *IntIterator) HasNext() bool {
return ii.index < len(ii.collection.items)
}
func (ii *IntIterator) Next() interface{} {
if ii.HasNext() {
item := ii.collection.items[ii.index]
ii.index++
return item
}
return nil
}
func main() {
collection := &IntCollection{items: []int{1, 2, 3, 4, 5}}
iterator := collection.CreateIterator()
for iterator.HasNext() {
fmt.Println(iterator.Next())
}
}
Chain of Responsibility Pattern
Pass request along chain of handlers.
Good: Chain of Responsibility
package main
import (
"fmt"
)
type Handler interface {
SetNext(Handler)
Handle(request string)
}
type ConcreteHandlerA struct {
next Handler
}
func (cha *ConcreteHandlerA) SetNext(handler Handler) {
cha.next = handler
}
func (cha *ConcreteHandlerA) Handle(request string) {
if request == "A" {
fmt.Println("Handler A handling request")
} else if cha.next != nil {
cha.next.Handle(request)
}
}
type ConcreteHandlerB struct {
next Handler
}
func (chb *ConcreteHandlerB) SetNext(handler Handler) {
chb.next = handler
}
func (chb *ConcreteHandlerB) Handle(request string) {
if request == "B" {
fmt.Println("Handler B handling request")
} else if chb.next != nil {
chb.next.Handle(request)
}
}
func main() {
handlerA := &ConcreteHandlerA{}
handlerB := &ConcreteHandlerB{}
handlerA.SetNext(handlerB)
handlerA.Handle("A")
handlerA.Handle("B")
}
Best Practices
- Choose Right Pattern: Match pattern to problem
- Keep Simple: Don’t over-complicate
- Document Intent: Explain pattern usage
- Test Thoroughly: Ensure correctness
- Avoid Premature Optimization: Use when needed
- Consider Alternatives: Patterns aren’t always best
- Refactor When Needed: Add patterns later
- Learn from Examples: Study implementations
Common Pitfalls
- Over-Engineering: Using patterns unnecessarily
- Wrong Pattern: Choosing inappropriate pattern
- Complexity: Patterns add complexity
- Performance: Some patterns have overhead
- Maintenance: Patterns can be hard to understand
Resources
Summary
Behavioral patterns manage object collaboration and responsibility. Use Observer for notifications, Strategy for algorithms, Command for requests, State for behavior changes, and Iterator for traversal. Choose patterns that solve real problems. Keep implementations simple and well-documented.
Comments