System Design
Design With Sid
design patterns

Factory Design Pattern - A Deep Dive

Factory Design Pattern - A Deep Dive

What is the Factory Design Pattern?

The Factory Design Pattern is a creational design pattern that provides an interface for creating objects in a superclass, but allows subclasses to alter the type of objects that will be created. This pattern is widely used when the exact type of object needs to be determined at runtime. It helps centralize the object creation logic and provides flexibility, making your system more maintainable and scalable.

The Problem Without Factory Pattern

Let’s take a simple example. Imagine we run a pizza shop, and we need to create a method orderPizza() to handle pizza orders. Initially, our method could look something like this:

Pizza orderPizza() {
    Pizza pizza = new Pizza();
    pizza.prepare();
    pizza.bake();
    pizza.cut();
    pizza.box();
    return pizza;
}

Now, the type of pizza is determined dynamically at runtime based on the user’s input. This works well in the beginning, but what happens when we need to make updates or add new types of pizza?

For example, imagine we receive pressure from the competition to add new pizza types or discontinue existing ones. If we keep adding more pizza types in this manner, we will have to keep changing the client code. Over time, this can lead to maintenance nightmares and unnecessarily complex code. Here’s how that might look after adding a few more pizza types:

Pizza orderPizza(String type) {
    Pizza pizza;

    if (type.equals("cheese")) {
        pizza = new CheesePizza();
    } else if (type.equals("greek")) {
        pizza = new GreekPizza();
    } else if (type.equals("pepperoni")) {
        pizza = new PepperoniPizza();
    } else if (type.equals("clam")) {
        pizza = new ClamPizza();
    } else if (type.equals("veggie")) {
        pizza = new VeggiePizza();
    }

    pizza.prepare();
    pizza.bake();
    pizza.cut();
    pizza.box();
    return pizza;
}

As we keep adding more types (clam, veggie, hawaiian, etc.), the orderPizza method grows longer and longer. This makes the code harder to maintain, test, and extend. It also violates the Open/Closed Principle of SOLID design, which states that software should be “open for extension but closed for modification.” By constantly modifying the method to accommodate new pizza types, we are not following this principle, and the code becomes more prone to errors and harder to extend in the future.

The Solution: Factory Pattern

To solve this problem, we can use the Factory Pattern. The factory pattern encapsulates the creation of objects into a separate class, which allows us to add new pizza types without changing the client code. Instead of modifying the orderPizza method each time we add a new pizza type, we create a PizzaFactory class responsible for instantiating the correct pizza type based on the input.

Here’s how we can implement the Factory Pattern:

1. Create a PizzaFactory Class

public class SimplePizzaFactory {
    public Pizza createPizza(String type) {
        Pizza pizza = null;

        if (type.equals("cheese")) {
            pizza = new CheesePizza();
        } else if (type.equals("pepperoni")) {
            pizza = new PepperoniPizza();
        } else if (type.equals("clam")) {
            pizza = new ClamPizza();
        } else if (type.equals("veggie")) {
            pizza = new VeggiePizza();
        }

        return pizza;
    }
}

In this case, the PizzaFactory class is responsible for creating the appropriate pizza type based on the input string. By doing this, we have encapsulated the object creation logic and can easily modify it without touching the main client code.

2. Update the PizzaStore Class

The PizzaStore class will now rely on the PizzaFactory to create pizzas:

public class PizzaStore {
    SimplePizzaFactory factory;

    public PizzaStore(SimplePizzaFactory factory) {
        this.factory = factory;
    }

    public Pizza orderPizza(String type) {
        Pizza pizza;

        pizza = factory.createPizza(type);
        pizza.prepare();
        pizza.bake();
        pizza.cut();
        pizza.box();

        return pizza;
    }
}

Now, the PizzaStore class no longer has the responsibility of knowing how to create the pizzas. It delegates that responsibility to the PizzaFactory, simplifying the logic inside PizzaStore.

Benefits of Using the Factory Pattern

  1. Encapsulation of Object Creation: The factory pattern encapsulates the logic of object creation. The PizzaStore no longer needs to know about the specific pizza classes like CheesePizza, PepperoniPizza, etc. It only interacts with the PizzaFactory and receives the correct pizza.

  2. Adhering to the Open/Closed Principle: By using the factory pattern, we can easily add new pizza types without modifying existing code. The PizzaStore class doesn't need to change; we only modify or extend the PizzaFactory. This helps us follow the Open/Closed Principle of SOLID design.

  3. Improved Maintainability: If we need to add new pizza types or change the pizza creation logic, we can do so without affecting other parts of the code. This reduces the risk of bugs when adding new features or pizza types.

  4. Easier Testing: With the factory pattern, it’s easier to mock or test the pizza creation process separately from the client code. We can test the PizzaFactory in isolation, ensuring that it creates the correct pizza types based on the input.

Conclusion

The Factory Pattern basically encapsulates the object creation logic and helps client code follow open/close principle making it less prone to bugs and easier maintenance.