Design Pattern (5) Simple Factory Pattern Complete Tutorial - Encapsulating Object Creation Logic
You can download the Design Pattern series code from this design_pattern repo.
Project Requirements
Imagine you’re developing an ordering system for a beverage shop. This system needs to dynamically create different types of beverage objects based on customer selections.
The core requirements of the system include:
- Support for multiple beverage types (black tea, green tea, etc.)
- Unified preparation process (add sugar, add ice, shake, package)
- Good extensibility for easily adding new beverage items in the future
Let’s first analyze the basic structure of the system through UML diagrams, then gradually delve into the implementation details.
Object-Oriented Analysis (OOA)
First, let’s examine the initial system design. In this version, all beverage creation logic is written directly within the order
method of the BeverageShop
class.

Initial Implementation Approach
The following code demonstrates the most straightforward implementation approach. We define a Beverage
interface that includes the basic steps of beverage preparation, then have concrete beverage classes (like BlackTea
, GreenTea
) implement this interface.
-
public protocol Beverage { func addSuger(level: Int) func addIce(level: Int) func shake() func packageUp() } public extension Beverage { func addSuger(level: Int) { print("[\(self)] addSuger \(level)") } func addIce(level: Int) { print("[\(self)] addIce \(level)") } func shake() { print("[\(self)] shake") } func packageUp() { print("[\(self)] packageUp") } } public class BlackTea: Beverage { } public class GreenTea: Beverage { } 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?.addSuger(level: 5) beverage?.addIce(level: 5) beverage?.shake() beverage?.packageUp() return beverage } } let beverageShop = BeverageShop() let blackTea = beverageShop.order(beverageName: "black tea") let greenTea = beverageShop.order(beverageName: "green tea")
-
interface Beverage { fun addSuger(level: Int) { println("[$this] addSuger $level") } fun addIce(level: Int) { println("[$this] addIce $level") } fun shake() { println("[$this] shake") } fun packageUp() { println("[$this] packageUp") } } class BlackTea: Beverage { } class GreenTea: Beverage { } class BeverageShop { fun order(beverageName: String): Beverage? { val beverage: Beverage? = when (beverageName) { "black tea" -> BlackTea() "green tea" -> GreenTea() else -> null } beverage?.addSuger(5) beverage?.addIce(5) beverage?.shake() beverage?.packageUp() return beverage } }
Identifying Problems: Recognizing Forces
Extensibility Issues Emerge
When the beverage shop’s business grows and the owner decides to add more drink options, problems arise. Every time we add a new beverage type, we must modify the order
method of BeverageShop
.
This approach violates an important software design principle: closed to modification, open to extension. Even worse, modifying the order
method might accidentally affect other stable, running code.
Separating Variable from Stable Code
The key to solving this problem is identifying which code changes frequently and which code remains stable. Let’s analyze:
Frequently Changing Code
Every time we add a beverage item, this switch/when block must be modified:
-
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() else -> null }
Stable Code
In contrast, the beverage preparation process is fixed. No matter how many types of beverages we add, these steps never change:
-
beverage?.addSuger(level: 5) beverage?.addIce(level: 5) beverage?.shake() beverage?.packageUp()
-
beverage?.addSuger(5) beverage?.addIce(5) beverage?.shake() beverage?.packageUp()
Solution: Introducing the Simple Factory Pattern
Since we’ve clearly identified the variable and stable code, the next step is to separate them. This is exactly what the Simple Factory Pattern excels at solving.
The core concept of the Simple Factory Pattern is: encapsulate object creation logic in an independent factory class. This way, when we need to add new products, we only need to modify the factory class without affecting other code that uses these objects.
Implementing the Solution
Simple Factory Pattern Structure
Before diving into implementation, let’s understand the standard structure of the Simple Factory Pattern:

The core of the Simple Factory Pattern is creating a specialized factory class responsible for handling all object creation logic. This factory class typically contains a static method or instance method that decides which concrete product to create based on input parameters.
Applying to the Beverage System
Now let’s apply the Simple Factory Pattern to the beverage ordering system. The redesigned system structure is as follows:

Object-Oriented Programming (OOP)
Refactored Implementation
Now we can start implementing the refactored system. The key change is introducing the BeverageFactory
class, which is specifically responsible for beverage object creation.
Key Points of Architectural Improvement
- Separation of Responsibilities:
BeverageFactory
handles object creation,BeverageShop
handles order processing - Dependency Injection:
BeverageShop
receives factory instances through its constructor, improving flexibility - Single Responsibility: Each class has a clear and single responsibility
-
open class BeverageFactory { public init() {} func createBeverage(beverageName: String) -> Beverage? { var beverage: Beverage? switch beverageName { case "black tea": beverage = BlackTea() case "green tea": beverage = GreenTea() default: break } return beverage } } public class BeverageShop { private let factory: BeverageFactory public init(factory: BeverageFactory) { self.factory = factory } public func order(beverageName: String) -> Beverage? { let beverage: Beverage? = factory.createBeverage(beverageName: beverageName) beverage?.addSuger(level: 5) beverage?.addIce(level: 5) beverage?.shake() beverage?.packageUp() return beverage } } let beverageShop = BeverageShop(factory: BeverageFactory()) let blackTea = beverageShop.order(beverageName: "black tea") let greenTea = beverageShop.order(beverageName: "green tea")
-
class BeverageFactory { fun createBeverage(beverageName: String): Beverage? { return when (beverageName) { "black tea" -> BlackTea() "green tea" -> GreenTea() else -> null } } } class BeverageShop(private val factory: BeverageFactory) { fun order(beverageName: String): Beverage? { val beverage: Beverage? = factory.createBeverage(beverageName) beverage?.addSuger(5) beverage?.addIce(5) beverage?.shake() beverage?.packageUp() return beverage } } val beverage = BeverageShop(BeverageFactory()) val blackTea = beverage.order("black tea") val greenTea = beverage.order("green tea")
Refactoring Results Analysis
Through the application of the Simple Factory Pattern, we successfully achieved the following goals:
- Code Separation: Successfully separated frequently changing and stable code
- Enhanced Extensibility: When adding beverage items, only need to modify
BeverageFactory
without affecting other code - Improved Maintainability: Each class has clearer responsibilities, reducing maintenance costs
- Test-Friendly: Can independently test factory logic and order processing logic
Important Note
Simple Factory isn’t actually a design pattern, but rather more like a programming idiom
Some developers do mistake this programming idiom for the Factory Pattern
Don’t ignore its usefulness just because Simple Factory isn’t a true pattern.
– Head First Design Pattern Ch.4 P.117
Although Simple Factory is not one of the GoF 23 design patterns, it is an important foundation for learning more complex factory patterns and is also a very practical programming technique in daily development.
Summary and Reflection
Learning Outcomes
Although the Simple Factory Pattern is not among the GoF 23 classic design patterns, it has important learning value:
- Simple and Easy to Understand: Suitable as an introduction to the factory pattern series
- High Practicality: Frequently used programming techniques in daily development
- Cultivating Good Habits: Trains us to identify and separate variable from stable code
Applied Design Principles
In implementing the Simple Factory Pattern, we applied the following important design principles:
- Encapsulate What Varies: Encapsulate frequently changing object creation logic in the factory
- Single Responsibility Principle: Each class has a clear and single responsibility
Next Steps in Learning
Having mastered the concept of Simple Factory, we’ve laid the foundation for learning more complex factory patterns. The next article will formally enter the first of the GoF 23 design patterns: Factory Method Pattern, exploring how to further enhance system flexibility and extensibility.
References
- Head First Design Patterns
- Design Patterns
- Advanced Design Patterns: Design Principles
- Programming Foundations: Design Patterns
- Design Patterns: Creational
- Waterball Pan - Design Pattern Journey
Note: If you have any suggestions, questions, or different ideas, feel free to leave a comment or send me an email. We can discuss and learn together🙂
Enjoy Reading This Article?
Here are some more articles you might like to read next: