Skip to main content
โšก Calmops

Behavioral Design Patterns

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

  1. Choose Right Pattern: Match pattern to problem
  2. Keep Simple: Don’t over-complicate
  3. Document Intent: Explain pattern usage
  4. Test Thoroughly: Ensure correctness
  5. Avoid Premature Optimization: Use when needed
  6. Consider Alternatives: Patterns aren’t always best
  7. Refactor When Needed: Add patterns later
  8. 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