Design Patterns: Factory Pattern

These notes were adapted from various readings detailed in the References section.


The Factory Method Pattern defines an interface for creating an object, but lets subclasses decide which class to instantiate. Factory method lets a class defer instantiation to subclass.

There can be other ways to instantiate new objects other than using a constructor in object oriented programming languages. There will be occasions where object instantiation may be complicated, with many parameters to remember, or a sequence of operations needed before actually being able to call the constructor. Sometimes, some of the object construction details shouldn’t be known publicly.

The Factory Pattern can help with making it easier to instantiate objects. It gives a way to encapsulate the instantiations of concrete types.

Your beverage venture was quite successful, and has matured. It is time for you to start your next business venture. You have decided to open up a Pizza shop with Joe. At first you have a simple Pizza machine that will bake, and assemble a pizza for you to tell to customer. It’s main method to process pizzas is orderPizza.

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

Your store has become popular quickly, and now you want to make more than one type of Pizza. Your suggestion for Joe is that now orderPizza can take in a string type that will allow the Pizza machine to make many different types of Pizza.

public orderPizza(String type) {
    let pizza;
    
    if (type === "cheese") {
        pizza = new CheesePizza();
    } else if(type === "greek") {
        pizza = new GreekPizza();
    } else if (type === "pepperoni") {
        pizza = new PepperoniPizza();
    } 
    
    pizza.prepare();
    pizza.bake();
    pizza.cut();
    pizza.box();
    
    return pizza;
}

This works well for a couple of weeks, until your shop begins to get more requests to make more different types of pizzas from your hungry customers! Joe mentions that a clam pizza, and a veggie pizza have been the most requested thus far. He also showed sales of Greek pizza not doing well.

You have agreed to introduce clam pizza, and veggie pizza. In addition to that, you and Joe agree to stop selling Greek pizza.

You go back and modify the Pizza machine to account for the changes.

public orderPizza(type) {
    let pizza;
    
    if (type === "cheese") {
        pizza = new CheesePizza();
    } else if (type === "pepperoni") {
        pizza = new PepperoniPizza();
    } else if (type === "clam") {
        pizza = new ClamPizza();
    } else if (type === "veggie") {
        pizza = new VeggiePizza();
    }
    
    pizza.prepare();
    pizza.bake();
    pizza.cut();
    pizza.box();
    
    return pizza;
}

Maintaining the Pizza machine is getting harder than what it really should be. You and Joe start to think about making the Pizza machine more friendly for extension. The initial thought is to move the object creation out of the orderPizza method.

Joe suggests instead to apply the factory pattern to create pizzas. You think this makes sense since the factory method is meant for handling details of object creation.

You go on ahead and implement a new class that specifically creates and returns the appropriate pizza object.

class SimplePizzaFactory {
    public createPizza(type) {
        let pizza;
        
        if (type === "cheese") {
            pizza = new CheesePizza();
        } else if (type === "pepperoni") {
            pizza = new PepperoniPizza();
        } else if (type === "clam") {
            pizza = new ClamPizza();
        } else if (type === "veggie") {
            pizza = new VeggiePizza();
        }
        
        return pizza;
    }
}

Now, we can modify the system to include the factory.

class PizzaStore {
    private factory;
    
    constructor(pizzaFactory) {
        this.factory = pizzaFactory;
    }
    
    public orderPizza(type) {
        const pizza = this.factory.createPizza(type);
        
        pizza.prepare();
        pizza.bake();
        pizza.cut();
        pizza.box();
        
        return pizza;
    }
}

When new pizzas are introduced, you can simply now modify SimplePizzaFactory to include new pizzas without touching the pizza machine.

This new model actually brings your business success. You are now much more agile to respond to customer needs. You and Joe have made a lot of money from this new venture!

Now it is time to take the business nationally! You agree with Joe to open up more stores across the country with regional differences. However, in order to maintain the quality of Pizzas, every store should get the same design of the Pizza machine.

You have concluded that what each store is allowed to have are different types of pizza factories to create their own regional variations of pizza. You feel like this is not hard at all since the Pizza machine can take any implementation of a pizza factory through dependency injection.

After some time, you decide to remove the factory class altogether, and turn the PizzaStore class into a base class with an abstract factory method instead.

class PizzaStore {
    public orderPizza(type) {
        const pizza = this.factory.createPizza(type);
        
        pizza.prepare();
        pizza.bake();
        pizza.cut();
        pizza.box();
        
        return pizza;
    }
    
    protected abstract createPizza(type);
}

Now, a store is required to extend the PizzaStore class and implement their own factory method to create pizzas. At the end, the method of assembling the pizza itself is still under control by the Pizza machine.

The new implementation not only achieves the same effect as with the factory class, but is now more compact. Here, only subclasses which actually implement the factory method can create the products!

References

  • Head First Design Patterns - Eric Freeman & Elisabeth Freeman - https://www.oreilly.com/library/view/head-first-design/0596007124/