Menu Close

Object Oriented Design Pattern: (Abstract) Factory Method & Template Method

Factory Method Explanation

The Factory Method design pattern is a creational pattern that you can use to outsource the creation of objects. The Factory creates object instances which all have the same interface and/or abstract base class. The Template Method design pattern is usually a good companion to help factories with the object creation.

The difference between the Factory Method and Abstract Factory Method design pattern is that the former will focus on a single factory only. It will hopefully become clear with the examples.

Use Cases:

  • You want to create an object which adheres to an interface or abstract class, without being concerned about the concrete implementation.
  • Create objects based on business logic.
  • You want components to be loosely coupled.

Pitfalls:

  • Your code can become complex. Use clear names and/or namespaces to avoid losing oversight.

Template Method

The Template Method design pattern is a pattern which is used to define steps of an algorithm so it can be followed more easily. The Template Method design pattern is actually a Behavioral pattern. In the following example we will create virtual pizza’s.

In the Java example, the pizza baking algorithm adds ingredients and bakes a pizza. The steps “addIngredients()” and “bakePizza()” can be altered in a sub-implementation of the IPizza interface. For example, the PizzaCaprese class overrides the bakePizza method and implements its own step of the pizza baking algorithm. See the comments written in the classes for more information.

Java example of the Factory Method and Template Method design patterns

Class diagram of Factory Method design pattern

The Pizza Factory

package com.chosengambit.designpatterns.factory;

public class PizzaFactory {
    
    public enum PizzaTypes {
        FUNGI, CAPRESE, MARGHERITA
    }
    
    public IPizza createPizza(PizzaTypes pizzaType) {
        IPizza pizza = null;
        switch(pizzaType) {
            case FUNGI -> {
                pizza = new PizzaFungi();
            }
            case CAPRESE -> {
                pizza = new PizzaCaprese();
            }
            case MARGHERITA -> {
                pizza = new PizzaMargherita();
            }
        }
                
        if (pizza != null) {
            // pizza is loosely coupled because of its IPizza interface
            // template methods, which are part of the pizza baking 'algorithm'
            pizza.addIngredients();
            pizza.bakePizza();    
        }
        return pizza;
    }
}
Java

IPizza interface

package com.chosengambit.designpatterns.factory;

public interface IPizza {
    public void addIngredients();
    public void bakePizza();
    public boolean isReady();
}
Java

AbstractPizza as base class for pizza’s

package com.chosengambit.designpatterns.factory;

/**
* Already implements IPizza interface, so subclasses don't have to
*/
abstract public class AbstractPizza implements IPizza {
    
    protected String[] ingredients;
    protected boolean ready = false;
    
    @Override
    abstract public void addIngredients();

    @Override
    public void bakePizza() {
        System.out.println(getClass().getSimpleName()+ " is now baking in the oven.");
    }
    
    @Override
    public boolean isReady() {
        return this.ready;
    }
}
Java

Pizza Caprese, an implementation of a pizza

package com.chosengambit.designpatterns.factory;

public class PizzaCaprese extends AbstractPizza {

    // This is the Template Method Design Pattern where a method of the 
    // predefined algorithm  is overridden
    @Override
    public void addIngredients() {
        this.ingredients = new String[] {"Tomato sause", "Mozzarella", "Garlic", "Pesto"};
    }

    @Override
    public void bakePizza() {
        System.out.println("Baking the Caprese a little extra.");
    }
}
Java

Another pizza implementation:

package com.chosengambit.designpatterns.factory;

public class PizzaFungi extends AbstractPizza {

    @Override
    public void addIngredients() {
        this.ingredients = new String[] {"Tomato sause", "Cheese", "Mushroom"};
    }
}
Java

And the last pizza implementation for this example:

package com.chosengambit.designpatterns.factory;

public class PizzaMargherita extends AbstractPizza {

    @Override
    public void addIngredients() {
        this.ingredients = new String[] {"Tomato sause", "Cheese"};
    }

    @Override
    public void bakePizza() {
        super.bakePizza();
        this.ready = true;
    }
}
Java

Our Main class creates the PizzaFactory, which then creates a pizza with the help of the Template Method design pattern.

package com.chosengambit.designpatterns;

import com.chosengambit.designpatterns.factory.IPizza;
import com.chosengambit.designpatterns.factory.PizzaFactory;

public class Main {

    public static void main(String[] args) {
        PizzaFactory pizzaFactory = new PizzaFactory();
        IPizza caprese =    pizzaFactory.createPizza(PizzaFactory.PizzaTypes.CAPRESE);
        IPizza fungi =      pizzaFactory.createPizza(PizzaFactory.PizzaTypes.FUNGI);
        IPizza margherita = pizzaFactory.createPizza(PizzaFactory.PizzaTypes.MARGHERITA); 
        System.out.println("Is the margherita ready: "+margherita.isReady());       
    }
}
Java
Output:

Baking the Caprese a little extra.
PizzaFungi is now baking in the oven.
PizzaMargherita is now baking in the oven.
Is the margherita ready: true

Python Example of the Abstract Factory Method design pattern

Imagine a transport company that wants to ship goods globally. The company has three transportation options: by road, water or air. In this example we assume that the the automated transport selection suffices the business needs.

To interpret the code, start in the main.py file, which is last in line here. In Example #1, the algorithm automatically chooses and constructs a vehicle based on the travel data we inserted.

Class Diagram

Class Diagram of Abstract Factory Template design pattern

Implementation

Directory structure

The Abstract Vehicle Factory is the parent of concrete factory implementations.

import abc
from abc import ABC
from FactoryTemplate.TravelData import TravelData
from FactoryTemplate import AbstractVehicle


class AbstractVehicleFactory(ABC):

    @staticmethod
    def get_vehicle_factory(travel_data: TravelData):
        """ Get a factory based on travel data """
        # if we need to travel more than our road threshold, we do not use the road
        if travel_data.distance < travel_data.ROAD_TRAVEL_THRESHOLD:
            from FactoryTemplate.Factories.RoadFactory import RoadFactory
            return RoadFactory()

        if travel_data.harbor_distance < travel_data.airport_distance:
            from FactoryTemplate.Factories.WaterFactory import WaterFactory
            return WaterFactory()

        from FactoryTemplate.Factories.AerialFactory import AerialFactory
        return AerialFactory()

    @staticmethod
    def auto_create_suitable_vehicle(travel_data: TravelData) -> AbstractVehicle:
        factory = AbstractVehicleFactory.get_vehicle_factory(travel_data)
        vehicle: AbstractVehicle = factory.create_vehicle(travel_data)
        return vehicle

    @abc.abstractmethod
    def create_vehicle(self, travel_data) -> AbstractVehicle:
        pass

Python

The Aerial factory is able to create aviation vehicles. The factory decides what kind of object to return based on the travel_data. If the cargo is small and the distance is not too far, it will decide to build a helicopter.

from FactoryTemplate.AbstractFactory import AbstractVehicleFactory
from FactoryTemplate.Vehicles.AerialVehicles import *


class AerialFactory(AbstractVehicleFactory):

    def create_vehicle(self, travel_data: TravelData) -> AbstractVehicle:
        vehicle = None

        if travel_data.cargo_size == TravelData.CARGO_SIZE_SMALL and travel_data.distance < 4000:
            vehicle = Helicopter()
        else:
            vehicle = CargoPlane()

        if vehicle is not None:
            # Template Methods of which the implementation can be altered by subclasses
            vehicle.create_body(travel_data)
            vehicle.assemble_parts(travel_data)
            vehicle.paint(travel_data)
        return vehicle
Python

The Road Factory is able to create vehicle suitable for the roads. The Factory will create and return a Van, Pickup or Truck based on the cargo size.

from FactoryTemplate.AbstractFactory import AbstractVehicleFactory
from FactoryTemplate.Vehicles.RoadVehicles import *


class RoadFactory(AbstractVehicleFactory):

    def create_vehicle(self, travel_data: TravelData) -> AbstractVehicle:
        vehicle = None

        if travel_data.cargo_size == TravelData.CARGO_SIZE_SMALL:
            vehicle = Van()
        if travel_data.cargo_size == TravelData.CARGO_SIZE_MEDIUM:
            vehicle = Pickup()
        if travel_data.cargo_size == TravelData.CARGO_SIZE_LARGE:
            vehicle = Truck()
        return self.__assemble(vehicle, travel_data)

    def create_specific_vehicle(self, vehicle_type: str) -> AbstractVehicle:
        vt = vehicle_type.lower()
        vehicle = None
        if vt == "van":
            vehicle = Van()
        if vt == "pickup" or vt == "pick-up":
            vehicle = Pickup()
        vehicle = Truck()
        return self.__assemble(vehicle)

    def __assemble(self, vehicle, travel_data=None) -> AbstractVehicle:
        if vehicle is not None and isinstance(vehicle, AbstractVehicle):
            vehicle.create_body(travel_data)
            vehicle.assemble_parts(travel_data)
            vehicle.paint(travel_data)
        return vehicle
Python

The Water Factory can create ships based on the cargo size.

from FactoryTemplate.AbstractFactory import AbstractVehicleFactory
from FactoryTemplate import AbstractVehicle
from FactoryTemplate.Vehicles.WaterVehicles import *


class WaterFactory(AbstractVehicleFactory):

    def create_vehicle(self, travel_data: TravelData) -> AbstractVehicle:
        vehicle = None

        if travel_data.cargo_size == TravelData.CARGO_SIZE_LARGE:
            vehicle = LargeShip()
        else:
            vehicle = MediumShip()

        if vehicle is not None:
            vehicle.create_body(travel_data)
            vehicle.assemble_parts(travel_data)
            vehicle.paint(travel_data)
        return vehicle
Python

The base class for all vehicles; air, road and water.

import abc
from abc import ABC


class AbstractVehicle(ABC):

    def __init__(self):
        print("We're creating a "+type(self).__name__)

    @abc.abstractmethod
    def create_body(self, travel_data=None):
        pass

    @abc.abstractmethod
    def assemble_parts(self, travel_data=None):
        pass

    @abc.abstractmethod
    def paint(self, travel_data=None):
        pass
Python

Aerial vehicles

from FactoryTemplate.TravelData import TravelData
from FactoryTemplate.AbstractVehicle import AbstractVehicle


class CargoPlane(AbstractVehicle):
    def create_body(self, travel_data: TravelData):
        print("Create aircraft fuselage")

    def assemble_parts(self, travel_data: TravelData):
        print("Assembling: Wings, jet engines, interior")

    def paint(self, travel_data: TravelData):
        print("Paint cargo plane cyan and white")


class Helicopter(AbstractVehicle):
    def create_body(self, travel_data: TravelData):
        print("Create helicopter fuselage")

    def assemble_parts(self, travel_data: TravelData):
        print("Assembling parts: Rotor, diesel engine, interior")

    def paint(self, travel_data: TravelData):
        print("Paint helicopter yellow")
Python

Road vehicles

from FactoryTemplate.TravelData import TravelData
from FactoryTemplate.AbstractVehicle import AbstractVehicle


class Van(AbstractVehicle):

    def create_body(self, travel_data: TravelData):
        print("Create small body")

    def assemble_parts(self, travel_data: TravelData):
        print("Assembling parts: Air conditioning, 2 doors, a sliding door, 4 wheels, engine, seats, steer")

    def paint(self, travel_data: TravelData):
        print("Paint it green")


class Pickup(AbstractVehicle):
    def create_body(self, travel_data: TravelData):
        print("Create body with loading platform")

    def assemble_parts(self, travel_data: TravelData):
        print("Assembling Pickup parts: Air conditioning, doors, 4 wheels, engine, seats, steer")

    def paint(self, travel_data: TravelData):
        print("Paint this pickup blue")


class Truck(AbstractVehicle):
    def create_body(self, travel_data: TravelData):
        print("Create large body for truck")

    def assemble_parts(self, travel_data: TravelData):
        print("Assembling Truck parts: Air conditioning, 2 doors, 18 wheels, diesel engine, seats, steer")

    def paint(self, travel_data: TravelData):
        print("Paint the truck black")
Python

Water vehicles

from FactoryTemplate.TravelData import TravelData
from FactoryTemplate.AbstractVehicle import AbstractVehicle


class MediumShip(AbstractVehicle):
    def create_body(self, travel_data: TravelData):
        print("Create medium sized hull")

    def assemble_parts(self, travel_data: TravelData):
        print("Assembling ship parts: Rudder, 50K HP diesel engine, bow")

    def paint(self, travel_data: TravelData):
        print("Paint ship red")


class LargeShip(AbstractVehicle):
    def create_body(self, travel_data: TravelData):
        print("Create large hull")

    def assemble_parts(self, travel_data: TravelData):
        print("Assembling ship parts: Rudder, 80K HP diesel engine, bow")

    def paint(self, travel_data: TravelData):
        print("Paint ship gray")
Python

The travel data class which is used to decide what vehicle should be built.

class TravelData:

    ROAD_TRAVEL_THRESHOLD = 2000        # the maximal travel distance for road vehicles
    EXPRESS_PACKAGE = False             # should this package be delivered quickly?
    CARGO_SIZE_SMALL = 1
    CARGO_SIZE_MEDIUM = 2
    CARGO_SIZE_LARGE = 3

    cargo_size = 0                      # size of cargo space needed
    distance = 0                        # distance to destination
    airport_distance = 0                # airport distance to destination
    harbor_distance = 0                 # harbor distance to destination
Python

Main.py: The application starts here.

from FactoryTemplate.AbstractFactory import *
from FactoryTemplate.Factories.RoadFactory import RoadFactory
from FactoryTemplate.Factories.WaterFactory import WaterFactory


try:
    # Example 1, automatically creating a vehicle, based on travel data
    print("Example 1")
    travel_data: TravelData = TravelData()
    travel_data.distance = 5000
    travel_data.cargo_size = 1
    travel_data.airport_distance = 500
    travel_data.harbor_distance = 650
    vehicle = AbstractVehicleFactory.auto_create_suitable_vehicle(travel_data)

    # Example 2, creating a ship, based on travel data
    print("Example 2")
    factory = WaterFactory()
    vehicle = factory.create_vehicle(travel_data)

    # Example 3, specific creation
    print("Example 3")
    factory = RoadFactory()
    vehicle = factory.create_specific_vehicle("truck")

except Exception as e:
    print(e)
Python

Output:

Example 1
We're creating a CargoPlane
Create aircraft fuselage
Assembling: Wings, jet engines, interior
Paint cargo plane cyan and white

Example 2
We're creating a MediumShip
Create medium sized hull
Assembling ship parts: Rudder, 50K HP diesel engine, bow
Paint ship red

Example 3
We're creating a Truck
Create large body for truck
Assembling Truck parts: Air conditioning, 2 doors, 18 wheels, diesel engine, seats, steer
Paint the truck black

Conclusion

In my opinion Example 1 of the Python Abstract Factory design pattern truly shows the power of the pattern. With the travel data we can make abstract decisions about what vehicle type to create. Example 2 and 3 show that we are still able to have more control over the instantiation of vehicles.

Although I have made some shortcuts with less important details of the implementation, I hopefully have explained the strength of the Factory Method design pattern and Abstract method design pattern clearly. Some checks and filters are not implemented in this example, e.g. the cargo size in the TravelData class should be checked on it being either a 1, 2 or 3 and so on. The abstraction of the object creation process can be controlled by business logic quite nicely.

References

Freeman, E., Bates, B., & Sierra, K. (2004). Head first design patterns.

Gamma, E., Helm, R., Johnson, R., & Vlissides, J. (1994). Design Patterns: Elements of Reusable Object-Oriented Software.

Wikipedia contributors. (2024, 10 september). Software design pattern. Wikipedia. https://en.wikipedia.org/wiki/Software_design_pattern

Related Posts