Baking with OO Goodness : Understanding factory methods

Ashutosh RathAshutosh Rath
6 min read

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

  1. Creator: Abstract class that defines the factory method

  2. Concrete Creator: Implements the factory method to produce concrete products

  3. Product: Interface that defines the product

  4. 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

A professional, simple Stick Man illustration of a pizza ingredients factory. The scene features several machines producing different pizza ingredients like dough, sauce, cheese, and various toppings. Stick Men workers are assembling pizzas by collecting ingredients from these machines and placing them on assembly lines. The factory is equipped with large industrial machines, each labeled with its respective ingredient. The Stick Men are depicted in simple factory attire, working efficiently. The scene is in black and white, emphasizing the ingredients and the assembly process.

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

  1. Abstract Factory: Interface declaring creation methods for products

  2. Concrete Factory: Implements creation methods for concrete products

  3. Abstract Product: Interface that defines a product type

  4. Concrete Product: Specific implementation of a product

  5. 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()

A lighthearted and fun Stick Man illustration of a pizza ingredients factory. The scene features cartoonish, less industrial machines producing pizza ingredients like dough, sauce, cheese, and toppings. The Stick Men workers are playfully assembling pizzas, with exaggerated actions like tossing the dough in the air or comically juggling cheese. The factory is more whimsical and not as bold, with softer lines and a more relaxed, casual atmosphere. The machines are labeled with their ingredients, and the overall feel is light and humorous, with the Stick Men wearing quirky factory attire. The scene is in black and white, with a fun, playful vibe.

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✌️

20
Subscribe to my newsletter

Read articles from Ashutosh Rath directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Ashutosh Rath
Ashutosh Rath