Design Pattern 5: Simple Factory Pattern - Centralized Object Creation for Dynamic Beverage Ordering Systems

Download the complete Design Pattern series code from the design_pattern repo.

Introduction: The Power of Centralized Object Creation

The Simple Factory Pattern is a creational design pattern that provides a centralized way to create objects without exposing the instantiation logic to the client. This pattern is particularly useful when you need to separate object creation logic from the rest of your application code.

Real-World Applications

The Simple Factory Pattern is widely used in:

  • Beverage Ordering Systems: Creating different types of drinks based on customer orders
  • Payment Processing: Creating different payment gateways (PayPal, Stripe, etc.)
  • Database Connections: Creating different database adapters
  • UI Components: Creating different types of UI elements
  • Game Development: Creating different types of game objects

Problem Statement: Dynamic Beverage Ordering System

Our goal is to create a beverage ordering system that can dynamically generate beverage objects based on user selections. Let’s start by analyzing the basic system structure through UML.

Object-Oriented Analysis (OOA)

  • public protocol Beverage {
        func addSugar(level: Int)
        func addIce(level: Int)
        func shake()
        func packageUp()
    }
    
    public extension Beverage {
        func addSugar(level: Int) {
            print("[\(self)] addSugar \(level)")
        }
    
        func addIce(level: Int) {
            print("[\(self)] addIce \(level)")
        }
    
        func shake() {
            print("[\(self)] shake")
        }
    
        func packageUp() {
            print("[\(self)] packageUp")
        }
    }
    
    public class BlackTea: Beverage {
        // Black tea specific implementation
    }
    
    public class GreenTea: Beverage {
        // Green tea specific implementation
    }
    
    public class BeverageShop {
        public init() {}
    
        public func order(beverageName: String) -> Beverage? {
            var beverage: Beverage?
    
            switch beverageName {
            case "black tea":
                beverage = BlackTea()
            case "green tea":
                beverage = GreenTea()
            default:
                break
            }
    
            beverage?.addSugar(level: 5)
            beverage?.addIce(level: 5)
            beverage?.shake()
            beverage?.packageUp()
    
            return beverage
        }
    }
    
    // Usage
    let beverageShop = BeverageShop()
    let blackTea = beverageShop.order(beverageName: "black tea")
    let greenTea = beverageShop.order(beverageName: "green tea")
    
  • interface Beverage {
        fun addSugar(level: Int) {
            println("[$this] addSugar $level")
        }
    
        fun addIce(level: Int) {
            println("[$this] addIce $level")
        }
    
        fun shake() {
            println("[$this] shake")
        }
    
        fun packageUp() {
            println("[$this] packageUp")
        }
    }
    
    class BlackTea: Beverage {
        // Black tea specific implementation
    }
    
    class GreenTea: Beverage {
        // Green tea specific implementation
    }
    
    class BeverageShop {
        fun order(beverageName: String): Beverage? {
            val beverage: Beverage? = when (beverageName) {
                "black tea" -> BlackTea()
                "green tea" -> GreenTea()
                else -> null
            }
    
            beverage?.addSugar(5)
            beverage?.addIce(5)
            beverage?.shake()
            beverage?.packageUp()
    
            return beverage
        }
    }
    
    // Usage
    val beverageShop = BeverageShop()
    val blackTea = beverageShop.order("black tea")
    val greenTea = beverageShop.order("green tea")
    

Identifying Design Forces

As the beverage shop adds more new drinks, we need to modify the order method, which can easily affect code that shouldn’t change. We need to identify and separate variable code from constant code.

Variable Code (Code that changes)

  • switch beverageName {
    case "black tea":
        beverage = BlackTea()
    case "green tea":
        beverage = GreenTea()
    // case "milk tea":
        // beverage = MilkTea()
    default:
        break
    }
    
  • val beverage: Beverage? = when (beverageName) {
        "black tea" -> BlackTea()
        "green tea" -> GreenTea()
        // "milk tea" -> MilkTea()
        else -> null
    }
    

Constant Code (Code that doesn’t change)

  • beverage?.addSugar(level: 5)
    beverage?.addIce(level: 5)
    beverage?.shake()
    beverage?.packageUp()
    
  • beverage?.addSugar(5)
    beverage?.addIce(5)
    beverage?.shake()
    beverage?.packageUp()
    

Applying Simple Factory Pattern Solution

The Simple Factory Pattern provides an elegant solution by centralizing the object creation logic in a separate factory class.

Simple Factory Pattern UML Structure

Key Components:

  • Product: The interface for the objects being created
  • Concrete Products: Specific implementations of the product
  • Factory: Centralized class responsible for creating objects
  • Client: Uses the factory to create objects

Implementation: Object-Oriented Programming (OOP)

Product Interface

interface Beverage {
    fun addSugar(level: Int) {
        println("[$this] addSugar $level")
    }

    fun addIce(level: Int) {
        println("[$this] addIce $level")
    }

    fun shake() {
        println("[$this] shake")
    }

    fun packageUp() {
        println("[$this] packageUp")
    }
}

Concrete Products

class BlackTea: Beverage {
    override fun toString(): String = "BlackTea"
}

class GreenTea: Beverage {
    override fun toString(): String = "GreenTea"
}

class MilkTea: Beverage {
    override fun toString(): String = "MilkTea"
}

Simple Factory

class BeverageFactory {
    fun createBeverage(beverageName: String): Beverage? {
        return when (beverageName.lowercase()) {
            "black tea" -> BlackTea()
            "green tea" -> GreenTea()
            "milk tea" -> MilkTea()
            else -> null
        }
    }
}

Updated Client

class BeverageShop(private val factory: BeverageFactory) {
    fun order(beverageName: String): Beverage? {
        val beverage = factory.createBeverage(beverageName)
        
        beverage?.addSugar(5)
        beverage?.addIce(5)
        beverage?.shake()
        beverage?.packageUp()
        
        return beverage
    }
}

// Usage
fun main() {
    val factory = BeverageFactory()
    val shop = BeverageShop(factory)
    
    val blackTea = shop.order("black tea")
    val greenTea = shop.order("green tea")
    val milkTea = shop.order("milk tea")
}

Advanced Implementation: Enhanced Factory with Validation

class EnhancedBeverageFactory {
    private val supportedBeverages = setOf("black tea", "green tea", "milk tea", "oolong tea")
    
    fun createBeverage(beverageName: String): Beverage? {
        if (!supportedBeverages.contains(beverageName.lowercase())) {
            println("Unsupported beverage: $beverageName")
            return null
        }
        
        return when (beverageName.lowercase()) {
            "black tea" -> BlackTea()
            "green tea" -> GreenTea()
            "milk tea" -> MilkTea()
            "oolong tea" -> OolongTea()
            else -> null
        }
    }
    
    fun getSupportedBeverages(): Set<String> = supportedBeverages.toSet()
}

class OolongTea: Beverage {
    override fun toString(): String = "OolongTea"
}

Real-World Example: Payment Gateway Factory

interface PaymentGateway {
    fun processPayment(amount: Double): Boolean
    fun getGatewayName(): String
}

class PayPalGateway: PaymentGateway {
    override fun processPayment(amount: Double): Boolean {
        println("Processing $amount via PayPal")
        return true
    }
    
    override fun getGatewayName(): String = "PayPal"
}

class StripeGateway: PaymentGateway {
    override fun processPayment(amount: Double): Boolean {
        println("Processing $amount via Stripe")
        return true
    }
    
    override fun getGatewayName(): String = "Stripe"
}

class PaymentGatewayFactory {
    fun createGateway(gatewayType: String): PaymentGateway? {
        return when (gatewayType.lowercase()) {
            "paypal" -> PayPalGateway()
            "stripe" -> StripeGateway()
            else -> null
        }
    }
}

class PaymentProcessor(private val factory: PaymentGatewayFactory) {
    fun processPayment(gatewayType: String, amount: Double): Boolean {
        val gateway = factory.createGateway(gatewayType)
        return gateway?.processPayment(amount) ?: false
    }
}

Best Practices and Considerations

1. Error Handling

// Good: Proper error handling
class RobustBeverageFactory {
    fun createBeverage(beverageName: String): Result<Beverage> {
        return try {
            val beverage = when (beverageName.lowercase()) {
                "black tea" -> BlackTea()
                "green tea" -> GreenTea()
                "milk tea" -> MilkTea()
                else -> throw IllegalArgumentException("Unsupported beverage: $beverageName")
            }
            Result.success(beverage)
        } catch (e: Exception) {
            Result.failure(e)
        }
    }
}

2. Factory Method with Caching

// Good: Factory with object caching
class CachedBeverageFactory {
    private val cache = mutableMapOf<String, Beverage>()
    
    fun createBeverage(beverageName: String): Beverage? {
        return cache.getOrPut(beverageName.lowercase()) {
            when (beverageName.lowercase()) {
                "black tea" -> BlackTea()
                "green tea" -> GreenTea()
                "milk tea" -> MilkTea()
                else -> return null
            }
        }
    }
}

3. Configuration-Based Factory

// Good: Configuration-driven factory
class ConfigurableBeverageFactory(private val config: Map<String, String>) {
    fun createBeverage(beverageName: String): Beverage? {
        val className = config[beverageName.lowercase()] ?: return null
        
        return try {
            Class.forName(className).getDeclaredConstructor().newInstance() as Beverage
        } catch (e: Exception) {
            null
        }
    }
}

// Usage
val config = mapOf(
    "black tea" to "com.example.BlackTea",
    "green tea" to "com.example.GreenTea",
    "milk tea" to "com.example.MilkTea"
)
val factory = ConfigurableBeverageFactory(config)

Performance Considerations

Approach Memory Usage Performance Maintainability Extensibility
Direct Instantiation Low High Low Low
Simple Factory Medium Medium High Medium
Factory Method Medium Medium High High
Abstract Factory High Medium High High
  • Factory Method: Creates objects without specifying exact classes
  • Abstract Factory: Creates families of related objects
  • Builder: Constructs complex objects step by step
  • Singleton: Ensures only one instance exists

Conclusion

The Simple Factory Pattern provides a powerful way to centralize object creation logic while separating variable code from constant code. Key benefits include:

  • Code Separation: Clear separation between object creation and business logic
  • Maintainability: Easy to modify object creation logic in one place
  • Flexibility: Easy to add new product types
  • Testability: Easier to test object creation logic separately

This pattern is essential for building maintainable applications where object creation logic needs to be centralized and managed effectively.




    Enjoy Reading This Article?

    Here are some more articles you might like to read next:

  • How to Use Multiple GitHub Accounts on One Computer: Complete SSH Setup Guide
  • Excalidraw AI: Create Professional Diagrams with Text Commands - Complete Guide
  • Complete macOS Development Environment Setup Guide for 2024
  • Design Pattern 28: Interpreter Pattern - Complete Guide with Examples
  • Design Pattern 27: Visitor Pattern - Complete Guide with Real-World IoT Examples