Table of Contents
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
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;
}
}
JavaIPizza interface
package com.chosengambit.designpatterns.factory;
public interface IPizza {
public void addIngredients();
public void bakePizza();
public boolean isReady();
}
JavaAbstractPizza 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;
}
}
JavaPizza 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.");
}
}
JavaAnother pizza implementation:
package com.chosengambit.designpatterns.factory;
public class PizzaFungi extends AbstractPizza {
@Override
public void addIngredients() {
this.ingredients = new String[] {"Tomato sause", "Cheese", "Mushroom"};
}
}
JavaAnd 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;
}
}
JavaOur 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());
}
}
JavaOutput: 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
Implementation
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
PythonThe 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
PythonThe 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
PythonThe 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
PythonThe 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
PythonAerial 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")
PythonRoad 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")
PythonWater 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")
PythonThe 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
PythonMain.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)
PythonOutput: 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