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
- 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
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