Baking with OO Goodness : Understanding factory methods


Introduction
“Baking with OO Goodness” - This chapter from Head First Design Patterns came in handy when I was building a data pipeline at my org. Have you ever found yourself with a growing mess of if-else
statements to create different object types? Or perhaps you've noticed that your codebase keeps expanding with new concrete classes, making maintenance increasingly difficult? If so, you've likely encountered problems that factory patterns were designed to solve.
In this blog post, we'll dive deep into two of the most useful creational design patterns: Factory Method and Abstract Factory. We'll explore their similarities, differences, and real-world applications using a data pipeline system as our practical example. Please keep your pen and notebook handy.
Grab your coffee🍵, put on your design thinking hat📑, and let's untangle these patterns together!
The Problem: Object Creation Gets Messy
Imagine you're building a pizza ordering system for a chain that has different styles of pizzas across various locations. Each location (New York, Chicago, etc.) has its own unique way of preparing pizzas, but the ordering process remains the same.
Your initial approach might be to create a centralized function that handles creating all types of pizzas based on style and type. As your chain grows, this function grows too... and not in a good way.
Let's look at what this might look like in code:
def create_pizza(style, type):
pizza = None
if style == "NY":
if type == "cheese":
pizza = NYStyleCheesePizza()
elif type == "veggie":
pizza = NYStyleVeggiePizza()
elif type == "clam":
pizza = NYStyleClamPizza()
elif type == "pepperoni":
pizza = NYStylePepperoniPizza()
elif style == "Chicago":
if type == "cheese":
pizza = ChicagoStyleCheesePizza()
elif type == "veggie":
pizza = ChicagoStyleVeggiePizza()
elif type == "clam":
pizza = ChicagoStyleClamPizza()
elif type == "pepperoni":
pizza = ChicagoStylePepperoniPizza()
else:
print("Error: invalid type of pizza")
return None
pizza.prepare()
pizza.bake()
pizza.cut()
pizza.box()
return pizza
Look at this monstrosity! Every time we add a new pizza style or type, we need to modify this function. This violates the open-closed principle (code should be open for extension but closed for modification). It's also difficult to maintain and test.
Now imagine a similar situation in a data pipeline system where we need to handle different data sources, process them differently, and upload them to various destinations. The complexity multiplies quickly!
Meet the Factory Method Pattern
The Factory Method pattern defines an interface for creating objects but lets subclasses decide which classes to instantiate. It lets a class defer instantiation to subclasses.
🎭 The Cast of Characters
Creator: Abstract class that defines the factory method
Concrete Creator: Implements the factory method to produce concrete products
Product: Interface that defines the product
Concrete Product: Specific implementation of the product
🍕 Pizza Example Reimagined
Instead of the giant if-else block, we can structure our pizza store like this:
class PizzaStore:
def order_pizza(self, type):
pizza = self.create_pizza(type) # Factory method!
pizza.prepare()
pizza.bake()
pizza.cut()
pizza.box()
return pizza
def create_pizza(self, type):
# This will be implemented by subclasses
pass
class NYPizzaStore(PizzaStore):
def create_pizza(self, type):
if type == "cheese":
return NYStyleCheesePizza()
elif type == "veggie":
return NYStyleVeggiePizza()
# ...other types
class ChicagoPizzaStore(PizzaStore):
def create_pizza(self, type):
if type == "cheese":
return ChicagoStyleCheesePizza()
elif type == "veggie":
return ChicagoStyleVeggiePizza()
# ...other types
Enter the Abstract Factory Pattern
While Factory Method focuses on creating a single product, Abstract Factory provides an interface for creating families of related or dependent objects without specifying their concrete classes.
🎭 The Cast of Characters
Abstract Factory: Interface declaring creation methods for products
Concrete Factory: Implements creation methods for concrete products
Abstract Product: Interface that defines a product type
Concrete Product: Specific implementation of a product
Client: Uses factories and products through abstract interfaces
🍕 Pizza Example with Ingredients
The Abstract Factory pattern shines when dealing with families of related objects. In our pizza example, each regional style might need its own ingredient set:
class PizzaIngredientFactory:
def create_dough(self):
pass
def create_sauce(self):
pass
def create_cheese(self):
pass
def create_veggies(self):
pass
def create_pepperoni(self):
pass
def create_clam(self):
pass
class NYPizzaIngredientFactory(PizzaIngredientFactory):
def create_dough(self):
return ThinCrustDough()
def create_sauce(self):
return MarinaraSauce()
def create_cheese(self):
return ReggianoCheese()
# ...other ingredients
class ChicagoPizzaIngredientFactory(PizzaIngredientFactory):
def create_dough(self):
return ThickCrustDough()
def create_sauce(self):
return PlumTomatoSauce()
def create_cheese(self):
return MozzarellaCheese()
# ...other ingredients
Now our pizzas can use these ingredient factories:
class Pizza:
def prepare(self):
pass
def bake(self):
pass
def cut(self):
pass
def box(self):
pass
class CheesePizza(Pizza):
def __init__(self, ingredient_factory):
self.ingredient_factory = ingredient_factory
def prepare(self):
self.dough = self.ingredient_factory.create_dough()
self.sauce = self.ingredient_factory.create_sauce()
self.cheese = self.ingredient_factory.create_cheese()
Factory Method vs Abstract Factory: Head-to-Head Comparison
When to Use Each Pattern
Use Factory Method when:
A class can't anticipate the type of objects it must create
A class wants its subclasses to specify the objects it creates
You want to localize the knowledge of which concrete class gets created
You have a simple product hierarchy
Use Abstract Factory when:
A system should be independent of how its products are created
A system should be configured with one of multiple families of products
You want to enforce constraints between related products
You want to provide a library of products without exposing implementations
Conclusion
Both the Factory Method and Abstract Factory patterns provide elegant solutions to object creation problems, but they serve different purposes:
Factory Method is like a specialized chef who knows exactly how to make one type of dish (e.g., a pizza chef who can make different pizza variants)
Abstract Factory is like a restaurant that employs different specialized chefs, equipment, and ingredients to create entire meals with consistent themes (e.g., an Italian restaurant)
Remember that design patterns are tools, not rules. Use them when they fit your problem, and don't be afraid to adapt them to your specific needs.
What design patterns have you used in your projects? Have you found other creative ways to handle object creation? Share your experiences in the comments below!
Reference
- Freeman, E., Robson, E., Sierra, K., & Bates, B. (2004). Head First Design Patterns. O'Reilly Media. (Chapter 4: The Factory Patterns)
P.S. - AI has been used to improve the vocabulary of the blog as I am no master in English. Peace✌️
Subscribe to my newsletter
Read articles from Ashutosh Rath directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
