The Factory Method is a creational design pattern that solves a recurring problem in object-oriented programming: how do you create objects when the exact type isn’t known until runtime? It provides a blueprint for creating objects in a parent class but empowers subclasses to modify the type of objects that will be created.
This approach is fundamental to building flexible and scalable systems, allowing you to introduce new types of objects without modifying the core application logic.
The Journey to the Factory Method
To appreciate the Factory Method, let’s understand the problem it solves. Imagine you’re building a logistics system. Initially, you might handle object creation with a simple conditional.
Stage 1: The Simple if/else
If only one part of your application needs to create transport objects, a simple function with if/elif/else is perfectly acceptable.
# product.py
class Truck:
def deliver(self):
print("Delivering by land in a truck.")
class Ship:
def deliver(self):
print("Delivering by sea in a ship.")
# main.py
def create_transport(transport_type: str):
if transport_type == "TRUCK":
return Truck()
elif transport_type == "SHIP":
return Ship()
return None
# Client code
transport = create_transport("TRUCK")
transport.deliver()
This is straightforward. But what happens when multiple parts of your application need this logic? You’d be duplicating that if/elif/else block everywhere, which is a maintenance nightmare.
Stage 2: The Simple Factory
To avoid duplication, you centralize the creation logic into a single class—the Simple Factory.
# simple_factory.py
class TransportFactory:
def create_transport(self, transport_type: str):
if transport_type == "TRUCK":
return Truck()
elif transport_type == "SHIP":
return Ship()
return None
# Client code (now much cleaner)
factory = TransportFactory()
transport = factory.create_transport("SHIP")
transport.deliver()
This is a huge improvement! We’ve centralized creation logic. However, a new business requirement emerges:
Business Requirement: Each transport type now needs unique, complex initialization parameters.
- A
Truckneeds aplate_number.- A
Shipneeds aport_code.- A
Planeneeds aflight_number.
Our Simple Factory starts to break down.
# simple_factory.py (The Messy Version)
class TransportFactory:
- def create_transport(self, transport_type: str):
+ def create_transport(self, transport_type: str, **kwargs):
if transport_type == "TRUCK":
- return Truck()
+ # Requires a 'plate_number'
+ if 'plate_number' not in kwargs:
+ raise ValueError("Truck requires a plate_number")
+ return Truck(kwargs['plate_number'])
elif transport_type == "SHIP":
- return Ship()
+ # Requires a 'port_code'
+ if 'port_code' not in kwargs:
+ raise ValueError("Ship requires a port_code")
+ return Ship(kwargs['port_code'])
return None
This factory is now a tangled mess. It violates two key SOLID principles:
- Single Responsibility Principle: It knows about every single product and its specific creation details.
- Open/Closed Principle: To add a new
Planetype, we must modify this factory class directly.
This is where the Factory Method shines.
The Solution: The Factory Method Pattern
The Factory Method delegates the responsibility of instantiation to subclasses. The parent class knows that an object must be created, but the subclasses decide which object to create.
Here is the classic structure:
classDiagram
class Creator {
<<abstract>>
+create_product(): Product
+some_operation()
}
class ConcreteCreatorA {
+create_product(): Product
}
class ConcreteCreatorB {
+create_product(): Product
}
class Product {
<<interface>>
+operation()
}
class ConcreteProductA {
+operation()
}
class ConcreteProductB {
+operation()
}
Creator --|> Product : creates
ConcreteCreatorA --|> Creator : inherits
ConcreteCreatorB --|> Creator : inherits
ConcreteCreatorA ..> ConcreteProductA : creates
ConcreteCreatorB ..> ConcreteProductB : creates
ConcreteProductA --|> Product : implements
ConcreteProductB --|> Product : implements
Let’s apply this to our logistics system.
Step 1: Define the Product Interface and Concrete Products
First, we define our products. They all share a common interface (ITransport).
# products.py
from abc import ABC, abstractmethod
class ITransport(ABC):
@abstractmethod
def deliver(self):
pass
class Truck(ITransport):
def __init__(self, plate_number: str):
self._plate_number = plate_number
print(f"Truck created with plate: {self._plate_number}")
def deliver(self):
print(f"Delivering by land with truck {self._plate_number}.")
class Ship(ITransport):
def __init__(self, port_code: int):
self._port_code = port_code
print(f"Ship created for port: {self._port_code}")
def deliver(self):
print(f"Delivering by sea from port {self._port_code}.")
class Plane(ITransport):
def __init__(self, flight_number: str):
self._flight_number = flight_number
print(f"Plane created for flight: {self._flight_number}")
def deliver(self):
print(f"Delivering by air via flight {self._flight_number}.")
Step 2: Define the Abstract Creator with the Factory Method
Next, we create our abstract Logistics class. It has an abstract create_transport method (the factory method) and a plan_delivery method that uses it.
# creators.py
from abc import ABC, abstractmethod
from products import ITransport
class Logistics(ABC):
"""
The Creator class declares the factory method that is supposed to return an
object of a Product class. The Creator's subclasses usually provide the
implementation of this method.
"""
@abstractmethod
def create_transport(self) -> ITransport:
"""This is the factory method."""
pass
def plan_delivery(self):
"""
The Creator's primary responsibility is not creating products. It usually
contains some core business logic that relies on Product objects, returned
by the factory method. Subclasses can indirectly change that business logic
by overriding the factory method and returning a different type of product.
"""
transport = self.create_transport()
print("Factory method created a transport.")
transport.deliver()
[!NOTE] The
plan_deliverymethod is decoupled from the concrete products. It works with any product that conforms to theITransportinterface, thanks to the factory method.
Step 3: Implement the Concrete Creators
Now, we create concrete subclasses. Each one knows exactly how to instantiate its specific product.
# concrete_creators.py
from creators import Logistics
from products import ITransport, Truck, Ship, Plane
class RoadLogistics(Logistics):
"""
Concrete Creators override the factory method in order to change the
resulting product's type.
"""
def __init__(self, plate_number: str):
self._plate_number = plate_number
def create_transport(self) -> ITransport:
return Truck(self._plate_number)
class SeaLogistics(Logistics):
def __init__(self, port_code: int):
self._port_code = port_code
def create_transport(self) -> ITransport:
return Ship(self._port_code)
class AirLogistics(Logistics):
def __init__(self, flight_number: str):
self._flight_number = flight_number
def create_transport(self) -> ITransport:
return Plane(self._flight_number)
Each creator handles its own complexity. RoadLogistics only knows about plate_number, and SeaLogistics only knows about port_code. The responsibilities are now cleanly separated.
Step 4: The Client Code
The client code now decides which creator to use based on its needs.
# main.py
from concrete_creators import RoadLogistics, SeaLogistics, AirLogistics
def client_code(logistics_creator: Logistics):
"""
The client code works with an instance of a concrete creator, albeit through
its base interface. As long as the client keeps working with the creator via
the base interface, you can pass it any creator's subclass.
"""
logistics_creator.plan_delivery()
print("App: Launched with RoadLogistics.")
client_code(RoadLogistics(plate_number="XYZ-123"))
print("\n" + "="*20 + "\n")
print("App: Launched with SeaLogistics.")
client_code(SeaLogistics(port_code=8080))
print("\n" + "="*20 + "\n")
print("App: Launched with AirLogistics.")
client_code(AirLogistics(flight_number="SN050"))
This is clean, maintainable, and adheres to the Open/Closed Principle. If we need to add a DroneLogistics, we just create the Drone product and DroneLogistics creator. No existing code needs to change.
Advanced Use Case: Combining Factory Method with Simple Factory
You might have noticed a new problem: the client code now has the responsibility of choosing the right Logistics creator.
# Client's burden
user_input = "road"
if user_input == "road":
creator = RoadLogistics(plate_number="ABC-456")
elif user_input == "sea":
creator = SeaLogistics(port_code=9001)
# ... and so on
We can solve this by using a Simple Factory whose job is to create the correct creator. This powerful combination gives us the best of both worlds: centralized creator selection and decoupled product instantiation.
Let’s create a LogisticsFactory.
# logistics_factory.py
from creators import Logistics
from concrete_creators import RoadLogistics, SeaLogistics, AirLogistics
class LogisticsFactory:
"""A Simple Factory for creating the correct Logistics creator."""
def create_logistics(self, transport_type: str, **kwargs) -> Logistics:
if transport_type.upper() == "ROAD":
return RoadLogistics(kwargs.get("plate_number", "DEFAULT-PLATE"))
elif transport_type.upper() == "SEA":
return SeaLogistics(kwargs.get("port_code", 9999))
elif transport_type.upper() == "AIR":
return AirLogistics(kwargs.get("flight_number", "DEFAULT-FLIGHT"))
else:
raise ValueError("Invalid transport type specified.")
# The final, clean client code
logistics_simple_factory = LogisticsFactory()
# Let's say we get this from user input or a config file
delivery_type = "AIR"
params = {"flight_number": "BA2490"}
# The factory handles the logic
logistics_provider = logistics_simple_factory.create_logistics(delivery_type, **params)
# Our client code remains clean and decoupled
logistics_provider.plan_delivery()
This layered pattern is extremely common in real-world applications. It provides a single, clean entry point while keeping the underlying product creation logic flexible and extensible.
Summary: When to Use the Factory Method
mindmap
root((Factory Method))
"When to Use?"
When a class can't anticipate the class of objects it must create.
When you want to provide users of your library/framework a way to extend its internal components.
When you want to save system resources by reusing existing objects instead of rebuilding them each time.
Pros
Avoids tight coupling between the creator and concrete products.
"Single Responsibility Principle: You can move the product creation code into one place."
"Open/Closed Principle: You can introduce new products without breaking existing client code."
Cons
The code can become more complicated since you need to introduce a lot of new subclasses.
Quiz Yourself: Test Your Understanding
Question: If you add a new `BicycleLogistics` creator, which files do you absolutely need to modify?
creators.py(the abstractLogisticsclass)products.pyconcrete_creators.pylogistics_factory.py(the Simple Factory)
Click for Solution
You would need to:
- Add a
Bicycleclass toproducts.py. - Add a
BicycleLogisticsclass toconcrete_creators.py. - Update the
LogisticsFactoryinlogistics_factory.pyto handle the "BICYCLE" type.
Logistics creator or any of the original client code that consumes the products. That's the power of the pattern!