Skip to main content
โšก Calmops

Creational Design Patterns

Creational Design Patterns

Creational patterns deal with object creation mechanisms. Go’s simplicity makes these patterns straightforward to implement.

Singleton Pattern

Ensure only one instance of a type exists.

Good: Thread-Safe Singleton

package main

import (
	"fmt"
	"sync"
)

type Database struct {
	connection string
}

var (
	instance *Database
	once     sync.Once
)

func GetDatabase() *Database {
	once.Do(func() {
		instance = &Database{
			connection: "localhost:5432",
		}
	})
	return instance
}

func main() {
	db1 := GetDatabase()
	db2 := GetDatabase()
	
	fmt.Println(db1 == db2) // true
}

Factory Pattern

Create objects without specifying exact classes.

Good: Factory Pattern

package main

import (
	"fmt"
)

type Logger interface {
	Log(message string)
}

type ConsoleLogger struct{}

func (cl *ConsoleLogger) Log(message string) {
	fmt.Println(message)
}

type FileLogger struct {
	filename string
}

func (fl *FileLogger) Log(message string) {
	fmt.Printf("File: %s\n", message)
}

// Factory function
func NewLogger(logType string) Logger {
	switch logType {
	case "console":
		return &ConsoleLogger{}
	case "file":
		return &FileLogger{filename: "app.log"}
	default:
		return &ConsoleLogger{}
	}
}

func main() {
	logger := NewLogger("console")
	logger.Log("Hello")
}

Builder Pattern

Construct complex objects step by step.

Good: Builder Pattern

package main

import (
	"fmt"
)

type User struct {
	Name     string
	Email    string
	Age      int
	Phone    string
	Address  string
}

type UserBuilder struct {
	user *User
}

func NewUserBuilder() *UserBuilder {
	return &UserBuilder{user: &User{}}
}

func (ub *UserBuilder) WithName(name string) *UserBuilder {
	ub.user.Name = name
	return ub
}

func (ub *UserBuilder) WithEmail(email string) *UserBuilder {
	ub.user.Email = email
	return ub
}

func (ub *UserBuilder) WithAge(age int) *UserBuilder {
	ub.user.Age = age
	return ub
}

func (ub *UserBuilder) WithPhone(phone string) *UserBuilder {
	ub.user.Phone = phone
	return ub
}

func (ub *UserBuilder) WithAddress(address string) *UserBuilder {
	ub.user.Address = address
	return ub
}

func (ub *UserBuilder) Build() *User {
	return ub.user
}

func main() {
	user := NewUserBuilder().
		WithName("Alice").
		WithEmail("[email protected]").
		WithAge(30).
		WithPhone("555-1234").
		Build()
	
	fmt.Printf("%+v\n", user)
}

Abstract Factory Pattern

Create families of related objects.

Good: Abstract Factory

package main

import (
	"fmt"
)

type Button interface {
	Click()
}

type Input interface {
	Focus()
}

type UIFactory interface {
	CreateButton() Button
	CreateInput() Input
}

// Windows implementation
type WindowsButton struct{}

func (wb *WindowsButton) Click() {
	fmt.Println("Windows button clicked")
}

type WindowsInput struct{}

func (wi *WindowsInput) Focus() {
	fmt.Println("Windows input focused")
}

type WindowsFactory struct{}

func (wf *WindowsFactory) CreateButton() Button {
	return &WindowsButton{}
}

func (wf *WindowsFactory) CreateInput() Input {
	return &WindowsInput{}
}

// Mac implementation
type MacButton struct{}

func (mb *MacButton) Click() {
	fmt.Println("Mac button clicked")
}

type MacInput struct{}

func (mi *MacInput) Focus() {
	fmt.Println("Mac input focused")
}

type MacFactory struct{}

func (mf *MacFactory) CreateButton() Button {
	return &MacButton{}
}

func (mf *MacFactory) CreateInput() Input {
	return &MacInput{}
}

func main() {
	var factory UIFactory = &WindowsFactory{}
	
	button := factory.CreateButton()
	input := factory.CreateInput()
	
	button.Click()
	input.Focus()
}

Prototype Pattern

Clone objects instead of creating new ones.

Good: Prototype Pattern

package main

import (
	"fmt"
)

type Document interface {
	Clone() Document
	Print()
}

type Report struct {
	Title   string
	Content string
	Author  string
}

func (r *Report) Clone() Document {
	return &Report{
		Title:   r.Title,
		Content: r.Content,
		Author:  r.Author,
	}
}

func (r *Report) Print() {
	fmt.Printf("Report: %s by %s\n", r.Title, r.Author)
}

func main() {
	original := &Report{
		Title:   "Annual Report",
		Content: "...",
		Author:  "Alice",
	}
	
	// Clone instead of creating new
	clone := original.Clone().(*Report)
	clone.Title = "Annual Report 2024"
	
	original.Print()
	clone.Print()
}

Object Pool Pattern

Reuse expensive objects.

Good: Object Pool

package main

import (
	"fmt"
	"sync"
)

type Connection struct {
	ID int
}

type ConnectionPool struct {
	mu          sync.Mutex
	available   chan *Connection
	inUse       map[int]*Connection
	nextID      int
	maxPoolSize int
}

func NewConnectionPool(size int) *ConnectionPool {
	pool := &ConnectionPool{
		available:   make(chan *Connection, size),
		inUse:       make(map[int]*Connection),
		maxPoolSize: size,
	}
	
	for i := 0; i < size; i++ {
		pool.available <- &Connection{ID: i}
	}
	
	return pool
}

func (cp *ConnectionPool) Get() *Connection {
	return <-cp.available
}

func (cp *ConnectionPool) Return(conn *Connection) {
	cp.available <- conn
}

func main() {
	pool := NewConnectionPool(3)
	
	conn1 := pool.Get()
	fmt.Printf("Got connection: %d\n", conn1.ID)
	
	conn2 := pool.Get()
	fmt.Printf("Got connection: %d\n", conn2.ID)
	
	pool.Return(conn1)
	fmt.Println("Returned connection")
	
	conn3 := pool.Get()
	fmt.Printf("Got connection: %d\n", conn3.ID)
}

Best Practices

  1. Choose Right Pattern: Use pattern that fits problem
  2. Keep Simple: Don’t over-engineer
  3. Document Intent: Explain why pattern is used
  4. Test Thoroughly: Ensure pattern works correctly
  5. Avoid Premature Optimization: Use patterns when needed
  6. Consider Alternatives: Patterns aren’t always best solution
  7. Refactor When Needed: Patterns can be added later
  8. Learn from Examples: Study real-world 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

Creational patterns handle object creation. Use Singleton for single instances, Factory for flexible creation, Builder for complex objects, and Prototype for cloning. Choose patterns that solve real problems. Keep implementations simple and well-documented.

Comments