What is Factory Pattern?
The Factory pattern is a creational design pattern that provides an interface for creating objects without specifying their exact classes. It’s like having a factory that produces different types of products based on what you request, but you don’t need to know the intricate details of how each product is made.
I’ll walk you through a practical scenario that demonstrates the power of the Factory pattern in Go.
Let’s start with a scenario: Payment Processing
Imagine you’re building an e-commerce platform that needs to support multiple payment methods: Credit Card, PayPal, and Cryptocurrency. Each payment method has different processing logic, but you want a unified way to create and use them.
Without Factory Pattern
Here’s how you might handle this without the Factory pattern:
type CreditCardPayment struct {
CardNumber string
CVV string
}
func (c *CreditCardPayment) ProcessPayment(amount float64) string {
return fmt.Sprintf("Processing $%.2f via Credit Card ending in %s",
amount, c.CardNumber[len(c.CardNumber)-4:])
}
type PayPalPayment struct {
Email string
}
func (p *PayPalPayment) ProcessPayment(amount float64) string {
return fmt.Sprintf("Processing $%.2f via PayPal account %s", amount, p.Email)
}
// In your main code, you'd have to know which concrete type to create
func main() {
var payment interface{}
paymentType := "creditcard" // This could come from user input
if paymentType == "creditcard" {
payment = &CreditCardPayment{CardNumber: "1234567890123456", CVV: "123"}
} else if paymentType == "paypal" {
payment = &PayPalPayment{Email: "[email protected]"}
}
// This approach gets messy quickly!
}
With Factory Pattern
Let’s refactor this using the Factory pattern:
package main
import "fmt"
// PaymentProcessor interface - our product interface
type PaymentProcessor interface {
ProcessPayment(amount float64) string
}
// Concrete implementations
type CreditCardPayment struct {
CardNumber string
CVV string
}
func (c *CreditCardPayment) ProcessPayment(amount float64) string {
return fmt.Sprintf("Processing $%.2f via Credit Card ending in %s",
amount, c.CardNumber[len(c.CardNumber)-4:])
}
type PayPalPayment struct {
Email string
}
func (p *PayPalPayment) ProcessPayment(amount float64) string {
return fmt.Sprintf("Processing $%.2f via PayPal account %s", amount, p.Email)
}
type CryptoPayment struct {
WalletAddress string
}
func (c *CryptoPayment) ProcessPayment(amount float64) string {
return fmt.Sprintf("Processing $%.2f via Crypto wallet %s", amount, c.WalletAddress)
}
// Factory function
func CreatePaymentProcessor(paymentType string, config map[string]string) PaymentProcessor {
switch paymentType {
case "creditcard":
return &CreditCardPayment{
CardNumber: config["cardNumber"],
CVV: config["cvv"],
}
case "paypal":
return &PayPalPayment{
Email: config["email"],
}
case "crypto":
return &CryptoPayment{
WalletAddress: config["walletAddress"],
}
default:
return nil
}
}
func main() {
// Configuration could come from user input, database, etc.
config := map[string]string{
"cardNumber": "1234567890123456",
"cvv": "123",
}
processor := CreatePaymentProcessor("creditcard", config)
if processor != nil {
result := processor.ProcessPayment(99.99)
fmt.Println(result)
}
// Easy to switch payment methods
paypalConfig := map[string]string{
"email": "[email protected]",
}
paypalProcessor := CreatePaymentProcessor("paypal", paypalConfig)
if paypalProcessor != nil {
result := paypalProcessor.ProcessPayment(149.99)
fmt.Println(result)
}
}
Advanced Factory with Registration
For even more flexibility, you can create a factory that allows registration of new payment types:
package main
import (
"fmt"
"sync"
)
type PaymentProcessor interface {
ProcessPayment(amount float64) string
}
type PaymentFactory func(config map[string]string) PaymentProcessor
type PaymentFactoryRegistry struct {
factories map[string]PaymentFactory
mu sync.RWMutex
}
func NewPaymentFactoryRegistry() *PaymentFactoryRegistry {
return &PaymentFactoryRegistry{
factories: make(map[string]PaymentFactory),
}
}
func (r *PaymentFactoryRegistry) Register(paymentType string, factory PaymentFactory) {
r.mu.Lock()
defer r.mu.Unlock()
r.factories[paymentType] = factory
}
func (r *PaymentFactoryRegistry) Create(paymentType string, config map[string]string) PaymentProcessor {
r.mu.RLock()
defer r.mu.RUnlock()
if factory, exists := r.factories[paymentType]; exists {
return factory(config)
}
return nil
}
// Global registry instance
var PaymentRegistry = NewPaymentFactoryRegistry()
func init() {
// Register payment factories
PaymentRegistry.Register("creditcard", func(config map[string]string) PaymentProcessor {
return &CreditCardPayment{
CardNumber: config["cardNumber"],
CVV: config["cvv"],
}
})
PaymentRegistry.Register("paypal", func(config map[string]string) PaymentProcessor {
return &PayPalPayment{
Email: config["email"],
}
})
}
func main() {
config := map[string]string{
"cardNumber": "1234567890123456",
"cvv": "123",
}
processor := PaymentRegistry.Create("creditcard", config)
if processor != nil {
result := processor.ProcessPayment(99.99)
fmt.Println(result)
}
}
Real-world Use Cases
Here’s where I commonly use the Factory pattern in Go:
- Database Drivers: Creating different database connections (MySQL, PostgreSQL, MongoDB) based on configuration
- HTTP Clients: Creating different HTTP clients with various configurations (timeout, retry logic, authentication)
- Loggers: Creating different logger instances (file, console, remote) based on environment
- Message Queue Producers: Creating producers for different message brokers (RabbitMQ, Kafka, Redis)
Benefits of Factory Pattern
- Decoupling: Client code doesn’t need to know about concrete classes
- Flexibility: Easy to add new product types without changing existing code
- Centralized Creation Logic: All object creation logic is in one place
- Configuration-driven: Object creation can be driven by configuration files or environment variables
Caveats
While the Factory pattern is powerful, it’s not always necessary:
- Over-engineering: Don’t use it if you only have one or two simple types
- Added Complexity: Introduces additional abstraction layers
- Runtime Errors: Type safety is reduced since creation happens at runtime
- Performance: Small overhead due to interface calls and type assertions
Thank you
Thank you for reading! The Factory pattern is one of the most practical design patterns in Go. It helps create clean, maintainable code that’s easy to extend. Please drop an email at [email protected] if you would like to share any feedback or suggestions. Peace!