Skip to main content
โšก Calmops

Structural Design Patterns

Structural Design Patterns

Structural patterns deal with object composition and relationships between entities.

Adapter Pattern

Convert interface to another interface clients expect.

Good: Adapter Pattern

package main

import (
	"fmt"
)

// Old interface
type LegacyPaymentSystem interface {
	MakePayment(amount float64) error
}

type LegacyProcessor struct{}

func (lp *LegacyProcessor) MakePayment(amount float64) error {
	fmt.Printf("Legacy payment: $%.2f\n", amount)
	return nil
}

// New interface
type ModernPaymentSystem interface {
	ProcessPayment(amount float64) error
}

// Adapter
type PaymentAdapter struct {
	legacy LegacyPaymentSystem
}

func (pa *PaymentAdapter) ProcessPayment(amount float64) error {
	return pa.legacy.MakePayment(amount)
}

func main() {
	legacy := &LegacyProcessor{}
	adapter := &PaymentAdapter{legacy: legacy}
	
	adapter.ProcessPayment(99.99)
}

Decorator Pattern

Add behavior to objects dynamically.

Good: Decorator Pattern

package main

import (
	"fmt"
)

type Component interface {
	Operation() string
}

type ConcreteComponent struct{}

func (cc *ConcreteComponent) Operation() string {
	return "ConcreteComponent"
}

type Decorator struct {
	component Component
}

type ConcreteDecoratorA struct {
	Decorator
}

func (cda *ConcreteDecoratorA) Operation() string {
	return fmt.Sprintf("DecoratorA(%s)", cda.component.Operation())
}

type ConcreteDecoratorB struct {
	Decorator
}

func (cdb *ConcreteDecoratorB) Operation() string {
	return fmt.Sprintf("DecoratorB(%s)", cdb.component.Operation())
}

func main() {
	component := &ConcreteComponent{}
	
	decorated := &ConcreteDecoratorA{
		Decorator: Decorator{component: component},
	}
	
	decorated = &ConcreteDecoratorA{
		Decorator: Decorator{component: decorated},
	}
	
	fmt.Println(decorated.Operation())
}

Facade Pattern

Provide simplified interface to complex subsystem.

Good: Facade Pattern

package main

import (
	"fmt"
)

// Complex subsystem
type CPU struct{}

func (c *CPU) Freeze() {
	fmt.Println("CPU freezing")
}

func (c *CPU) Jump(position int) {
	fmt.Println("CPU jumping to", position)
}

type Memory struct{}

func (m *Memory) Load(position int, data []byte) {
	fmt.Println("Memory loading data at", position)
}

type HardDrive struct{}

func (hd *HardDrive) Read(lba int, size int) []byte {
	fmt.Println("HardDrive reading")
	return []byte("data")
}

// Facade
type Computer struct {
	cpu      *CPU
	memory   *Memory
	hardDrive *HardDrive
}

func NewComputer() *Computer {
	return &Computer{
		cpu:       &CPU{},
		memory:    &Memory{},
		hardDrive: &HardDrive{},
	}
}

func (c *Computer) StartComputer() {
	fmt.Println("Starting computer...")
	c.cpu.Freeze()
	c.memory.Load(0, c.hardDrive.Read(0, 1024))
	c.cpu.Jump(0)
	fmt.Println("Computer started")
}

func main() {
	computer := NewComputer()
	computer.StartComputer()
}

Proxy Pattern

Provide surrogate for another object.

Good: Proxy Pattern

package main

import (
	"fmt"
)

type Subject interface {
	Request() string
}

type RealSubject struct{}

func (rs *RealSubject) Request() string {
	return "RealSubject response"
}

type Proxy struct {
	realSubject *RealSubject
}

func (p *Proxy) Request() string {
	if p.realSubject == nil {
		fmt.Println("Creating RealSubject")
		p.realSubject = &RealSubject{}
	}
	
	fmt.Println("Proxy: Logging request")
	return p.realSubject.Request()
}

func main() {
	proxy := &Proxy{}
	fmt.Println(proxy.Request())
	fmt.Println(proxy.Request())
}

Composite Pattern

Compose objects into tree structures.

Good: Composite Pattern

package main

import (
	"fmt"
)

type Component interface {
	Operation() string
	Add(Component)
	Remove(Component)
	GetChild(int) Component
}

type Leaf struct {
	name string
}

func (l *Leaf) Operation() string {
	return l.name
}

func (l *Leaf) Add(Component)      {}
func (l *Leaf) Remove(Component)   {}
func (l *Leaf) GetChild(int) Component { return nil }

type Composite struct {
	name     string
	children []Component
}

func (c *Composite) Operation() string {
	result := c.name + "["
	for i, child := range c.children {
		if i > 0 {
			result += ","
		}
		result += child.Operation()
	}
	result += "]"
	return result
}

func (c *Composite) Add(child Component) {
	c.children = append(c.children, child)
}

func (c *Composite) Remove(child Component) {
	// Remove implementation
}

func (c *Composite) GetChild(index int) Component {
	return c.children[index]
}

func main() {
	leaf1 := &Leaf{name: "Leaf1"}
	leaf2 := &Leaf{name: "Leaf2"}
	
	composite := &Composite{name: "Composite"}
	composite.Add(leaf1)
	composite.Add(leaf2)
	
	fmt.Println(composite.Operation())
}

Bridge Pattern

Decouple abstraction from implementation.

Good: Bridge Pattern

package main

import (
	"fmt"
)

// Implementation
type Renderer interface {
	RenderCircle(radius float64)
}

type VectorRenderer struct{}

func (vr *VectorRenderer) RenderCircle(radius float64) {
	fmt.Printf("Drawing circle with radius %.2f using vectors\n", radius)
}

type RasterRenderer struct{}

func (rr *RasterRenderer) RenderCircle(radius float64) {
	fmt.Printf("Drawing circle with radius %.2f using raster\n", radius)
}

// Abstraction
type Shape interface {
	Draw()
}

type Circle struct {
	renderer Renderer
	radius   float64
}

func (c *Circle) Draw() {
	c.renderer.RenderCircle(c.radius)
}

func main() {
	vectorCircle := &Circle{
		renderer: &VectorRenderer{},
		radius:   5.0,
	}
	vectorCircle.Draw()
	
	rasterCircle := &Circle{
		renderer: &RasterRenderer{},
		radius:   5.0,
	}
	rasterCircle.Draw()
}

Flyweight Pattern

Share common state between objects.

Good: Flyweight Pattern

package main

import (
	"fmt"
)

type Flyweight interface {
	Operation(extrinsicState string)
}

type ConcreteFlyweight struct {
	sharedState string
}

func (cf *ConcreteFlyweight) Operation(extrinsicState string) {
	fmt.Printf("Shared: %s, Extrinsic: %s\n", cf.sharedState, extrinsicState)
}

type FlyweightFactory struct {
	flyweights map[string]Flyweight
}

func NewFlyweightFactory() *FlyweightFactory {
	return &FlyweightFactory{
		flyweights: make(map[string]Flyweight),
	}
}

func (ff *FlyweightFactory) GetFlyweight(sharedState string) Flyweight {
	if fw, ok := ff.flyweights[sharedState]; ok {
		return fw
	}
	
	fw := &ConcreteFlyweight{sharedState: sharedState}
	ff.flyweights[sharedState] = fw
	return fw
}

func main() {
	factory := NewFlyweightFactory()
	
	fw1 := factory.GetFlyweight("shared1")
	fw1.Operation("extrinsic1")
	
	fw2 := factory.GetFlyweight("shared1")
	fw2.Operation("extrinsic2")
	
	fmt.Println(fw1 == fw2) // true - same object
}

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

Structural patterns compose objects into larger structures. Use Adapter to convert interfaces, Decorator to add behavior, Facade to simplify complexity, and Proxy for controlled access. Choose patterns that solve real problems. Keep implementations simple and well-documented.

Comments