Go Composition Patterns
Composition patterns in Go are powerful design techniques that enable code reuse, flexibility, and maintainability through the combination of different types and behaviors. Unlike traditional inheritance-based object-oriented programming, Go emphasizes composition over inheritance, providing more flexible and maintainable code structures. Understanding composition patterns is crucial for writing idiomatic Go code and implementing sophisticated software architectures.
Understanding Composition Patterns in Go
What Are Composition Patterns?
Composition patterns in Go are design techniques that combine different types and behaviors to create more complex and flexible systems. These patterns provide:
- Code reuse - Share functionality across different types
- Flexibility - Easily modify and extend behavior
- Maintainability - Clean, modular code structures
- Testability - Easy to test individual components
- Scalability - Build complex systems from simple components
Go's Composition Philosophy
Go's composition approach has several key characteristics:
Composition over Inheritance
Go encourages building complex types by combining simpler types rather than inheriting from them.
Interface-based Design
Interfaces provide contracts for composition and enable loose coupling.
Embedding and Delegation
Struct embedding enables automatic method promotion and field access.
Mixin Patterns
Reusable components can be embedded in multiple types.
Mixin Patterns and Traits
Understanding Mixins
Mixin Definition
Mixins are reusable components that provide specific functionality to multiple types.
Trait-like Behavior
Go's embedding enables trait-like behavior for code reuse.
package main
import "fmt"
func main() {
// Mixin patterns and traits examples
fmt.Println("Mixin patterns and traits examples:")
// Timestamp mixin
type Timestamp struct {
CreatedAt string
UpdatedAt string
}
func (t *Timestamp) SetCreatedAt(time string) {
t.CreatedAt = time
}
func (t *Timestamp) SetUpdatedAt(time string) {
t.UpdatedAt = time
}
func (t *Timestamp) GetCreatedAt() string {
return t.CreatedAt
}
func (t *Timestamp) GetUpdatedAt() string {
return t.UpdatedAt
}
// Logger mixin
type Logger struct {
LogLevel string
}
func (l *Logger) Log(message string) {
fmt.Printf("[%s] %s\n", l.LogLevel, message)
}
func (l *Logger) SetLogLevel(level string) {
l.LogLevel = level
}
func (l *Logger) GetLogLevel() string {
return l.LogLevel
}
// Validator mixin
type Validator struct {
Rules []string
}
func (v *Validator) AddRule(rule string) {
v.Rules = append(v.Rules, rule)
}
func (v *Validator) Validate(data interface{}) bool {
fmt.Printf("Validating data with %d rules\n", len(v.Rules))
return true // Simplified validation
}
func (v *Validator) GetRules() []string {
return v.Rules
}
// Use mixins in different structs
type User struct {
ID int
Username string
Email string
Timestamp // Embedded mixin
Logger // Embedded mixin
Validator // Embedded mixin
}
type Product struct {
ID int
Name string
Price float64
Timestamp // Embedded mixin
Logger // Embedded mixin
Validator // Embedded mixin
}
// Create user with mixins
user := &User{
ID: 1,
Username: "alice",
Email: "[email protected]",
Timestamp: Timestamp{},
Logger: Logger{LogLevel: "INFO"},
Validator: Validator{},
}
// Use mixin methods
user.SetCreatedAt("2023-12-07T10:00:00Z")
user.SetUpdatedAt("2023-12-07T10:30:00Z")
user.Log("User created successfully")
user.AddRule("username must be unique")
user.AddRule("email must be valid")
user.Validate(user.Username)
fmt.Printf("User: %s (%s)\n", user.Username, user.Email)
fmt.Printf("Created: %s\n", user.GetCreatedAt())
fmt.Printf("Updated: %s\n", user.GetUpdatedAt())
fmt.Printf("Rules: %v\n", user.GetRules())
// Output:
// [INFO] User created successfully
// Validating data with 2 rules
// User: alice ([email protected])
// Created: 2023-12-07T10:00:00Z
// Updated: 2023-12-07T10:30:00Z
// Rules: [username must be unique email must be valid]
// Create product with mixins
product := &Product{
ID: 1,
Name: "Laptop",
Price: 999.99,
Timestamp: Timestamp{},
Logger: Logger{LogLevel: "DEBUG"},
Validator: Validator{},
}
// Use mixin methods
product.SetCreatedAt("2023-12-07T11:00:00Z")
product.SetUpdatedAt("2023-12-07T11:15:00Z")
product.Log("Product created successfully")
product.AddRule("price must be positive")
product.AddRule("name must not be empty")
product.Validate(product.Name)
fmt.Printf("Product: %s - $%.2f\n", product.Name, product.Price)
fmt.Printf("Created: %s\n", product.GetCreatedAt())
fmt.Printf("Updated: %s\n", product.GetUpdatedAt())
fmt.Printf("Rules: %v\n", product.GetRules())
// Output:
// [DEBUG] Product created successfully
// Validating data with 2 rules
// Product: Laptop - $999.99
// Created: 2023-12-07T11:00:00Z
// Updated: 2023-12-07T11:15:00Z
// Rules: [price must be positive name must not be empty]
}
Advanced Mixin Patterns
Conditional Mixins
Mixins that provide different behavior based on conditions.
Composable Mixins
Mixins that can be combined in different ways.
package main
import "fmt"
func main() {
// Advanced mixin patterns examples
fmt.Println("Advanced mixin patterns examples:")
// Cache mixin
type Cache struct {
data map[string]interface{}
}
func (c *Cache) Get(key string) (interface{}, bool) {
if c.data == nil {
c.data = make(map[string]interface{})
}
value, exists := c.data[key]
return value, exists
}
func (c *Cache) Set(key string, value interface{}) {
if c.data == nil {
c.data = make(map[string]interface{})
}
c.data[key] = value
}
func (c *Cache) Clear() {
c.data = make(map[string]interface{})
}
// Metrics mixin
type Metrics struct {
counters map[string]int
}
func (m *Metrics) Increment(name string) {
if m.counters == nil {
m.counters = make(map[string]int)
}
m.counters[name]++
}
func (m *Metrics) GetCount(name string) int {
if m.counters == nil {
return 0
}
return m.counters[name]
}
func (m *Metrics) GetAllCounts() map[string]int {
return m.counters
}
// Service mixin
type Service struct {
name string
running bool
}
func (s *Service) Start() {
s.running = true
fmt.Printf("Service %s started\n", s.name)
}
func (s *Service) Stop() {
s.running = false
fmt.Printf("Service %s stopped\n", s.name)
}
func (s *Service) IsRunning() bool {
return s.running
}
// Composable service with mixins
type UserService struct {
Service // Embedded service
Cache // Embedded cache
Metrics // Embedded metrics
}
func (us *UserService) GetUser(id string) (interface{}, bool) {
us.Metrics.Increment("get_user_calls")
// Try cache first
if user, found := us.Cache.Get(id); found {
us.Metrics.Increment("cache_hits")
return user, true
}
// Simulate database lookup
user := map[string]interface{}{
"id": id,
"name": "User " + id,
}
us.Cache.Set(id, user)
us.Metrics.Increment("cache_misses")
return user, true
}
// Create user service
userService := &UserService{
Service: Service{name: "UserService"},
Cache: Cache{},
Metrics: Metrics{},
}
// Start service
userService.Start()
// Use service
user1, found := userService.GetUser("123")
if found {
fmt.Printf("Retrieved user: %v\n", user1)
}
// Output: Retrieved user: map[id:123 name:User 123]
user2, found := userService.GetUser("123") // Should hit cache
if found {
fmt.Printf("Retrieved user (cached): %v\n", user2)
}
// Output: Retrieved user (cached): map[id:123 name:User 123]
// Check metrics
fmt.Printf("Get user calls: %d\n", userService.GetCount("get_user_calls"))
fmt.Printf("Cache hits: %d\n", userService.GetCount("cache_hits"))
fmt.Printf("Cache misses: %d\n", userService.GetCount("cache_misses"))
// Output:
// Get user calls: 2
// Cache hits: 1
// Cache misses: 1
// Stop service
userService.Stop()
}
Decorator Patterns
Understanding Decorator Patterns
Decorator Definition
Decorators wrap objects to add new functionality without modifying the original object.
Composition-based Decorators
Go's embedding enables clean decorator implementations.
package main
import "fmt"
func main() {
// Decorator patterns examples
fmt.Println("Decorator patterns examples:")
// Define interface
type Coffee interface {
Cost() float64
Description() string
}
// Base coffee
type SimpleCoffee struct{}
func (sc SimpleCoffee) Cost() float64 {
return 2.0
}
func (sc SimpleCoffee) Description() string {
return "Simple coffee"
}
// Milk decorator
type MilkDecorator struct {
Coffee // Embedded coffee
}
func (md MilkDecorator) Cost() float64 {
return md.Coffee.Cost() + 0.5
}
func (md MilkDecorator) Description() string {
return md.Coffee.Description() + ", with milk"
}
// Sugar decorator
type SugarDecorator struct {
Coffee // Embedded coffee
}
func (sd SugarDecorator) Cost() float64 {
return sd.Coffee.Cost() + 0.3
}
func (sd SugarDecorator) Description() string {
return sd.Coffee.Description() + ", with sugar"
}
// Vanilla decorator
type VanillaDecorator struct {
Coffee // Embedded coffee
}
func (vd VanillaDecorator) Cost() float64 {
return vd.Coffee.Cost() + 0.7
}
func (vd VanillaDecorator) Description() string {
return vd.Coffee.Description() + ", with vanilla"
}
// Create decorated coffees
simpleCoffee := SimpleCoffee{}
fmt.Printf("Simple coffee: %s - $%.2f\n", simpleCoffee.Description(), simpleCoffee.Cost())
// Output: Simple coffee: Simple coffee - $2.00
milkCoffee := MilkDecorator{Coffee: simpleCoffee}
fmt.Printf("Milk coffee: %s - $%.2f\n", milkCoffee.Description(), milkCoffee.Cost())
// Output: Milk coffee: Simple coffee, with milk - $2.50
sugarMilkCoffee := SugarDecorator{Coffee: milkCoffee}
fmt.Printf("Sugar milk coffee: %s - $%.2f\n", sugarMilkCoffee.Description(), sugarMilkCoffee.Cost())
// Output: Sugar milk coffee: Simple coffee, with milk, with sugar - $2.80
vanillaSugarMilkCoffee := VanillaDecorator{Coffee: sugarMilkCoffee}
fmt.Printf("Vanilla sugar milk coffee: %s - $%.2f\n", vanillaSugarMilkCoffee.Description(), vanillaSugarMilkCoffee.Cost())
// Output: Vanilla sugar milk coffee: Simple coffee, with milk, with sugar, with vanilla - $3.50
// HTTP handler decorator example
type Handler interface {
Handle(request string) string
}
type BasicHandler struct{}
func (bh BasicHandler) Handle(request string) string {
return "Basic response for: " + request
}
type LoggingDecorator struct {
Handler // Embedded handler
}
func (ld LoggingDecorator) Handle(request string) string {
fmt.Printf("Logging: Handling request: %s\n", request)
response := ld.Handler.Handle(request)
fmt.Printf("Logging: Response: %s\n", response)
return response
}
type AuthDecorator struct {
Handler // Embedded handler
}
func (ad AuthDecorator) Handle(request string) string {
fmt.Printf("Auth: Checking authentication for: %s\n", request)
response := ad.Handler.Handle(request)
fmt.Printf("Auth: Authentication successful\n")
return response
}
// Create decorated handler
handler := LoggingDecorator{
Handler: AuthDecorator{
Handler: BasicHandler{},
},
}
// Use decorated handler
response := handler.Handle("GET /users")
fmt.Printf("Final response: %s\n", response)
// Output:
// Logging: Handling request: GET /users
// Auth: Checking authentication for: GET /users
// Auth: Authentication successful
// Logging: Response: Basic response for: GET /users
// Final response: Basic response for: GET /users
}
Strategy Patterns
Understanding Strategy Patterns
Strategy Definition
Strategy patterns define a family of algorithms and make them interchangeable.
Interface-based Strategies
Go's interfaces provide a clean way to implement strategy patterns.
package main
import "fmt"
func main() {
// Strategy patterns examples
fmt.Println("Strategy patterns examples:")
// Define strategy interface
type PaymentStrategy interface {
Pay(amount float64) string
}
// Credit card strategy
type CreditCardStrategy struct {
CardNumber string
CVV string
}
func (ccs CreditCardStrategy) Pay(amount float64) string {
return fmt.Sprintf("Paid $%.2f with credit card ending in %s", amount, ccs.CardNumber[len(ccs.CardNumber)-4:])
}
// PayPal strategy
type PayPalStrategy struct {
Email string
}
func (pps PayPalStrategy) Pay(amount float64) string {
return fmt.Sprintf("Paid $%.2f with PayPal account %s", amount, pps.Email)
}
// Bank transfer strategy
type BankTransferStrategy struct {
AccountNumber string
RoutingNumber string
}
func (bts BankTransferStrategy) Pay(amount float64) string {
return fmt.Sprintf("Paid $%.2f with bank transfer from account %s", amount, bts.AccountNumber)
}
// Cryptocurrency strategy
type CryptoStrategy struct {
WalletAddress string
Currency string
}
func (cs CryptoStrategy) Pay(amount float64) string {
return fmt.Sprintf("Paid $%.2f with %s to wallet %s", amount, cs.Currency, cs.WalletAddress[:8]+"...")
}
// Context that uses strategy
type Order struct {
ID int
Amount float64
Payment PaymentStrategy
}
func (o Order) ProcessPayment() string {
return o.Payment.Pay(o.Amount)
}
// Create orders with different payment strategies
orders := []Order{
{
ID: 1,
Amount: 99.99,
Payment: CreditCardStrategy{CardNumber: "1234567890123456", CVV: "123"},
},
{
ID: 2,
Amount: 149.99,
Payment: PayPalStrategy{Email: "[email protected]"},
},
{
ID: 3,
Amount: 299.99,
Payment: BankTransferStrategy{AccountNumber: "987654321", RoutingNumber: "123456789"},
},
{
ID: 4,
Amount: 199.99,
Payment: CryptoStrategy{WalletAddress: "1A2B3C4D5E6F7G8H9I0J", Currency: "Bitcoin"},
},
}
// Process payments with different strategies
fmt.Println("Processing payments:")
for _, order := range orders {
fmt.Printf("Order %d: %s\n", order.ID, order.ProcessPayment())
}
// Output:
// Processing payments:
// Order 1: Paid $99.99 with credit card ending in 3456
// Order 2: Paid $149.99 with PayPal account [email protected]
// Order 3: Paid $299.99 with bank transfer from account 987654321
// Order 4: Paid $199.99 with Bitcoin to wallet 1A2B3C4D...
// Dynamic strategy selection
type PaymentProcessor struct {
strategies map[string]PaymentStrategy
}
func (pp *PaymentProcessor) RegisterStrategy(name string, strategy PaymentStrategy) {
if pp.strategies == nil {
pp.strategies = make(map[string]PaymentStrategy)
}
pp.strategies[name] = strategy
}
func (pp *PaymentProcessor) ProcessPayment(method string, amount float64) (string, error) {
strategy, exists := pp.strategies[method]
if !exists {
return "", fmt.Errorf("payment method %s not supported", method)
}
return strategy.Pay(amount), nil
}
// Create payment processor
processor := &PaymentProcessor{}
// Register strategies
processor.RegisterStrategy("credit_card", CreditCardStrategy{CardNumber: "1111222233334444", CVV: "456"})
processor.RegisterStrategy("paypal", PayPalStrategy{Email: "[email protected]"})
processor.RegisterStrategy("crypto", CryptoStrategy{WalletAddress: "9Z8Y7X6W5V4U3T2S1R0Q", Currency: "Ethereum"})
// Process payments dynamically
fmt.Println("\nDynamic payment processing:")
methods := []string{"credit_card", "paypal", "crypto"}
for _, method := range methods {
result, err := processor.ProcessPayment(method, 50.0)
if err != nil {
fmt.Printf("Error: %v\n", err)
} else {
fmt.Printf("Method %s: %s\n", method, result)
}
}
// Output:
// Dynamic payment processing:
// Method credit_card: Paid $50.00 with credit card ending in 4444
// Method paypal: Paid $50.00 with PayPal account [email protected]
// Method crypto: Paid $50.00 with Ethereum to wallet 9Z8Y7X6W...
}
Factory Patterns
Understanding Factory Patterns
Factory Definition
Factory patterns create objects without specifying their exact class.
Interface-based Factories
Go's interfaces provide a clean way to implement factory patterns.
package main
import "fmt"
func main() {
// Factory patterns examples
fmt.Println("Factory patterns examples:")
// Define product interface
type Animal interface {
MakeSound() string
Move() string
}
// Define factory interface
type AnimalFactory interface {
CreateAnimal() Animal
}
// Concrete products
type Dog struct {
Name string
Breed string
}
func (d Dog) MakeSound() string {
return "Woof!"
}
func (d Dog) Move() string {
return "Running on four legs"
}
type Cat struct {
Name string
Breed string
}
func (c Cat) MakeSound() string {
return "Meow!"
}
func (c Cat) Move() string {
return "Walking gracefully"
}
type Bird struct {
Name string
Species string
}
func (b Bird) MakeSound() string {
return "Tweet!"
}
func (b Bird) Move() string {
return "Flying through the air"
}
// Concrete factories
type DogFactory struct{}
func (df DogFactory) CreateAnimal() Animal {
return Dog{Name: "Generic Dog", Breed: "Mixed"}
}
type CatFactory struct{}
func (cf CatFactory) CreateAnimal() Animal {
return Cat{Name: "Generic Cat", Breed: "Mixed"}
}
type BirdFactory struct{}
func (bf BirdFactory) CreateAnimal() Animal {
return Bird{Name: "Generic Bird", Species: "Mixed"}
}
// Use factory pattern
factories := []AnimalFactory{
DogFactory{},
CatFactory{},
BirdFactory{},
}
fmt.Println("Creating animals with factories:")
for i, factory := range factories {
animal := factory.CreateAnimal()
fmt.Printf("Animal %d: %s - %s\n", i+1, animal.MakeSound(), animal.Move())
}
// Output:
// Creating animals with factories:
// Animal 1: Woof! - Running on four legs
// Animal 2: Meow! - Walking gracefully
// Animal 3: Tweet! - Flying through the air
// Parameterized factory
type ParameterizedAnimalFactory struct{}
func (paf ParameterizedAnimalFactory) CreateAnimal(animalType string) (Animal, error) {
switch animalType {
case "dog":
return Dog{Name: "Buddy", Breed: "Golden Retriever"}, nil
case "cat":
return Cat{Name: "Whiskers", Breed: "Persian"}, nil
case "bird":
return Bird{Name: "Tweety", Species: "Canary"}, nil
default:
return nil, fmt.Errorf("unknown animal type: %s", animalType)
}
}
// Use parameterized factory
paramFactory := ParameterizedAnimalFactory{}
animalTypes := []string{"dog", "cat", "bird", "fish"}
fmt.Println("\nCreating animals with parameterized factory:")
for _, animalType := range animalTypes {
animal, err := paramFactory.CreateAnimal(animalType)
if err != nil {
fmt.Printf("Error creating %s: %v\n", animalType, err)
} else {
fmt.Printf("%s: %s - %s\n", animalType, animal.MakeSound(), animal.Move())
}
}
// Output:
// Creating animals with parameterized factory:
// dog: Woof! - Running on four legs
// cat: Meow! - Walking gracefully
// bird: Tweet! - Flying through the air
// Error creating fish: unknown animal type: fish
// Abstract factory pattern
type Vehicle interface {
Start() string
Stop() string
}
type Car struct {
Model string
}
func (c Car) Start() string {
return fmt.Sprintf("Starting %s car", c.Model)
}
func (c Car) Stop() string {
return fmt.Sprintf("Stopping %s car", c.Model)
}
type Motorcycle struct {
Model string
}
func (m Motorcycle) Start() string {
return fmt.Sprintf("Starting %s motorcycle", m.Model)
}
func (m Motorcycle) Stop() string {
return fmt.Sprintf("Stopping %s motorcycle", m.Model)
}
type VehicleFactory interface {
CreateCar() Vehicle
CreateMotorcycle() Vehicle
}
type LuxuryVehicleFactory struct{}
func (lvf LuxuryVehicleFactory) CreateCar() Vehicle {
return Car{Model: "BMW X5"}
}
func (lvf LuxuryVehicleFactory) CreateMotorcycle() Vehicle {
return Motorcycle{Model: "Harley Davidson"}
}
type EconomyVehicleFactory struct{}
func (evf EconomyVehicleFactory) CreateCar() Vehicle {
return Car{Model: "Toyota Corolla"}
}
func (evf EconomyVehicleFactory) CreateMotorcycle() Vehicle {
return Motorcycle{Model: "Honda CBR"}
}
// Use abstract factory
factories2 := []VehicleFactory{
LuxuryVehicleFactory{},
EconomyVehicleFactory{},
}
fmt.Println("\nCreating vehicles with abstract factory:")
for i, factory := range factories2 {
car := factory.CreateCar()
motorcycle := factory.CreateMotorcycle()
fmt.Printf("Factory %d:\n", i+1)
fmt.Printf(" Car: %s\n", car.Start())
fmt.Printf(" Motorcycle: %s\n", motorcycle.Start())
}
// Output:
// Creating vehicles with abstract factory:
// Factory 1:
// Car: Starting BMW X5 car
// Motorcycle: Starting Harley Davidson motorcycle
// Factory 2:
// Car: Starting Toyota Corolla car
// Motorcycle: Starting Honda CBR motorcycle
}
What You've Learned
Congratulations! You now have a comprehensive understanding of Go's composition patterns:
Mixin Patterns and Traits
- Understanding mixin patterns and their benefits
- Implementing reusable components through embedding
- Creating trait-like behavior in Go
- Building composable and flexible systems
Decorator Patterns
- Understanding decorator patterns and their use cases
- Implementing decorators using embedding
- Creating flexible and extensible object hierarchies
- Building clean and maintainable decorator chains
Strategy Patterns
- Understanding strategy patterns and polymorphism
- Implementing interchangeable algorithms
- Creating flexible and configurable systems
- Building dynamic strategy selection mechanisms
Factory Patterns
- Understanding factory patterns and object creation
- Implementing different types of factories
- Creating flexible object creation systems
- Building parameterized and abstract factories
Key Concepts
- Composition over inheritance - Building complex types from simpler ones
- Mixin patterns - Reusable components through embedding
- Decorator patterns - Adding functionality without modification
- Strategy patterns - Interchangeable algorithms
- Factory patterns - Flexible object creation
Next Steps
You now have a solid foundation in Go's composition patterns. In the next section, we'll explore advanced OOP patterns, which combine all the concepts we've learned to create sophisticated software architectures.
Understanding composition patterns is crucial for building maintainable, flexible, and scalable applications. These concepts form the foundation for all the more advanced programming techniques we'll cover in the coming chapters.
Ready to learn about advanced OOP patterns? Let's explore sophisticated design patterns and learn how to build complex, maintainable systems!