As applications grow, the way we create objects can become a major source of complexity. A simple new keyword can quickly spiral into a web of if-else statements, making your code rigid, fragile, and difficult to extend.
The Factory Method is a creational design pattern that tackles this problem head-on. It provides a blueprint for creating objects but lets the subclasses decide which exact class to instantiate. This simple shift in responsibility is the key to building decoupled and scalable systems.
Let’s break down how this pattern works, why it’s an improvement over a Simple Factory, and how to implement it with practical, visual examples.
mindmap
root((Factory Method))
The Problem
- Complex object creation logic
- `if-else` chains
- Violates Open/Closed Principle
The Solution
- Defer instantiation to subclasses
- An abstract `creator` class
- Concrete `creator` subclasses
Structure
- Product Interface
- Concrete Products
- Abstract Creator (with Factory Method)
- Concrete Creators
Key Benefits
- Decoupling
- Extensibility (Single Responsibility)
- Follows SOLID principles
Advanced Use
- Combining with Simple Factory
The Problem: When a Simple Factory Isn’t Enough
In a previous guide, we explored the Simple Factory. It’s a great pattern for centralizing object creation. However, it has a breaking point. Consider a TransportFactory that creates different types of transport vehicles.
// Simple Factory - Initial Version
public class TransportFactory {
public Transport createTransport(String type) {
if ("TRUCK".equalsIgnoreCase(type)) {
return new Truck();
} else if ("SHIP".equalsIgnoreCase(type)) {
return new Ship();
}
return null;
}
}
This works well until the business requirements change. What if creating a Truck now requires a plateNumber, and creating a Ship requires a portCode? The factory becomes more complex.
[!WARNING] As new products with unique creation logic are added, the central factory class must be modified. This violates the Open/Closed Principle, which states that software entities should be open for extension but closed for modification.
Let’s see how the TransportFactory gets messy.
// Simple Factory - After new requirements
public class TransportFactory {
- public Transport createTransport(String type) {
- if ("TRUCK".equalsIgnoreCase(type)) {
- return new Truck();
- } else if ("SHIP".equalsIgnoreCase(type)) {
- return new Ship();
- }
- return null;
- }
+ // This method signature is already a problem. How do we pass different params?
+ public Transport createTransport(String type, Map<String, Object> params) {
+ if ("TRUCK".equalsIgnoreCase(type)) {
+ String plateNumber = (String) params.get("plateNumber");
+ return new Truck(plateNumber);
+ } else if ("SHIP".equalsIgnoreCase(type)) {
+ String portCode = (String) params.get("portCode");
+ return new Ship(portCode);
+ } else if ("AIRPLANE".equalsIgnoreCase(type)) {
+ // A new type requires another modification
+ String flightNumber = (String) params.get("flightNumber");
+ return new Airplane(flightNumber);
+ }
+ return null;
+ }
}
The factory is now bloated, error-prone (due to type casting from a Map), and a maintenance nightmare. This is the exact problem the Factory Method pattern solves.
The Solution: Delegating Creation to Subclasses
The Factory Method pattern introduces a simple but powerful idea: instead of a single class deciding which object to create, we delegate that decision to subclasses.
The structure involves four main components:
- Product Interface (
Transport): Defines the interface for the objects the factory method creates. - Concrete Products (
Truck,Ship): Implement the Product interface. - Creator (Abstract Class) (
Logistics): Declares the factory method (createTransport), which returns an object of the Product type. It can also contain business logic that relies on the product. - Concrete Creators (
RoadLogistics,SeaLogistics): Override the factory method to return an instance of a specific Concrete Product.
graph TD
subgraph Creators
A[Logistics <br> <font size=1><i>abstract</i></font><br>+ createTransport()<br>+ planDelivery()]
B[RoadLogistics <br> + createTransport()]
C[SeaLogistics <br> + createTransport()]
end
subgraph Products
D[Transport <br> <font size=1><i>interface</i></font><br>+ deliver()]
E[Truck <br> + deliver()]
F[Ship <br> + deliver()]
end
A -- defines --> D
B -- inherits from --> A
C -- inherits from --> A
E -- implements --> D
F -- implements --> D
B -- creates --> E
C -- creates --> F
style A fill:#f9f,stroke:#333,stroke-width:2px
style D fill:#ccf,stroke:#333,stroke-width:2px
Building a Logistics System with Factory Method
Let’s build this system from the ground up. Here’s the project structure we’ll create:
src/
└── com/
└── example/
├── products/
│ ├── Transport.java
│ ├── Truck.java
│ ├── Ship.java
│ └── Airplane.java
├── creators/
│ ├── Logistics.java
│ ├── RoadLogistics.java
│ ├── SeaLogistics.java
│ └── AirLogistics.java
└── Main.java
Step 1: Define the Product Interface and Concrete Products
First, we define what our products do. Then, we create concrete implementations, each with its own specific constructor.
// src/com/example/products/Transport.java
public interface Transport {
void deliver();
}
// src/com/example/products/Truck.java
public class Truck implements Transport {
private final String plateNumber;
public Truck(String plateNumber) {
this.plateNumber = plateNumber;
System.out.println("Truck created with plate: " + plateNumber);
}
@Override
public void deliver() {
System.out.println("Delivering by land in a truck.");
}
}
// src/com/example/products/Ship.java
public class Ship implements Transport {
private final String portCode;
public Ship(String portCode) {
this.portCode = portCode;
System.out.println("Ship created for port: " + portCode);
}
@Override
public void deliver() {
System.out.println("Delivering by sea in a container ship.");
}
}
// src/com/example/products/Airplane.java
public class Airplane implements Transport {
private final String flightNumber;
public Airplane(String flightNumber) {
this.flightNumber = flightNumber;
System.out.println("Airplane created for flight: " + flightNumber);
}
@Override
public void deliver() {
System.out.println("Delivering by air in a cargo plane.");
}
}
Step 2: Define the Abstract Creator and Concrete Creators
Next, we create the Logistics class. It has some business logic (planDelivery) that depends on a Transport object, but it doesn’t know how to create it. It delegates that job to its subclasses via the abstract createTransport method.
// src/com/example/creators/Logistics.java
public abstract class Logistics {
// This is the core business logic. It works with a product but doesn't
// know which concrete product it's working with.
public void planDelivery() {
Transport t = createTransport();
System.out.println("Transport created via factory method.");
t.deliver();
}
// This is the FACTORY METHOD.
// Subclasses MUST provide an implementation.
public abstract Transport createTransport();
}
Now, each concrete creator provides its own implementation of the factory method.
// src/com/example/creators/RoadLogistics.java
public class RoadLogistics extends Logistics {
private final String plateNumber;
public RoadLogistics(String plateNumber) {
this.plateNumber = plateNumber;
}
@Override
public Transport createTransport() {
// RoadLogistics knows that it must create a Truck.
return new Truck(this.plateNumber);
}
}
// src/com/example/creators/SeaLogistics.java
public class SeaLogistics extends Logistics {
private final String portCode;
public SeaLogistics(String portCode) {
this.portCode = portCode;
}
@Override
public Transport createTransport() {
// SeaLogistics knows that it must create a Ship.
return new Ship(this.portCode);
}
}
// src/com/example/creators/AirLogistics.java
public class AirLogistics extends Logistics {
private final String flightNumber;
public AirLogistics(String flightNumber) {
this.flightNumber = flightNumber;
}
@Override
public Transport createTransport() {
// AirLogistics knows that it must create an Airplane.
return new Airplane(this.flightNumber);
}
}
Step 3: The Client Code
The client code now decides which creator it needs based on the context. Once it has the creator, it can call the business logic (planDelivery) without ever knowing about the concrete product (Truck, Ship, etc.).
// src/com/example/Main.java
public class Main {
private static Logistics logistics;
public static void main(String[] args) {
// Based on some configuration or input, we choose the creator.
String transportType = "road"; // This could come from a config file, user input, etc.
if ("road".equalsIgnoreCase(transportType)) {
logistics = new RoadLogistics("PLATE-1234");
} else if ("sea".equalsIgnoreCase(transportType)) {
logistics = new SeaLogistics("PORT-5678");
} else if ("air".equalsIgnoreCase(transportType)) {
logistics = new AirLogistics("FLIGHT-9012");
} else {
throw new IllegalArgumentException("Unknown transport type");
}
// We call the business logic on the creator.
// The creator handles the product instantiation internally.
logistics.planDelivery();
}
}
Notice that the client is now decoupled from the concrete products, but it’s still coupled to the concrete creators. Can we do better?
Advanced Move: Combining Factory Method with a Simple Factory
In many real-world scenarios, you can combine patterns to achieve even greater flexibility. We can use a Simple Factory to hide the if-else logic for choosing the creator.
Let’s create a LogisticsFactory whose only job is to produce the correct Logistics creator instance.
// src/com/example/creators/LogisticsFactory.java
import java.util.Map;
public class LogisticsFactory {
public static Logistics createLogistics(String type, Map<String, String> params) {
if ("road".equalsIgnoreCase(type)) {
return new RoadLogistics(params.get("plateNumber"));
} else if ("sea".equalsIgnoreCase(type)) {
return new SeaLogistics(params.get("portCode"));
} else if ("air".equalsIgnoreCase(type)) {
return new AirLogistics(params.get("flightNumber"));
}
return null;
}
}
Now, our client code becomes incredibly clean and decoupled.
// src/com/example/Main.java
public class Main {
- private static Logistics logistics;
-
public static void main(String[] args) {
- // Based on some configuration or input, we choose the creator.
- String transportType = "road"; // This could come from a config file, user input, etc.
-
- if ("road".equalsIgnoreCase(transportType)) {
- logistics = new RoadLogistics("PLATE-1234");
- } else if ("sea".equalsIgnoreCase(transportType)) {
- logistics = new SeaLogistics("PORT-5678");
- } else if ("air".equalsIgnoreCase(transportType)) {
- logistics = new AirLogistics("FLIGHT-9012");
- } else {
- throw new IllegalArgumentException("Unknown transport type");
- }
-
- // We call the business logic on the creator.
- // The creator handles the product instantiation internally.
- logistics.planDelivery();
+ // The client is now completely decoupled from concrete creators and products.
+ // It only knows about the abstract Logistics class and the LogisticsFactory.
+ Map<String, String> params = Map.of("plateNumber", "PLATE-ABCD");
+ Logistics roadLogistics = LogisticsFactory.createLogistics("road", params);
+ if (roadLogistics != null) {
+ roadLogistics.planDelivery();
+ }
+
+ System.out.println("\n-----------------\n");
+
+ params = Map.of("flightNumber", "FLIGHT-XYZ");
+ Logistics airLogistics = LogisticsFactory.createLogistics("air", params);
+ if (airLogistics != null) {
+ airLogistics.planDelivery();
+ }
}
}
[!TIP] Combining patterns is a sign of mature software design. Here, a Simple Factory acts as a front-end to a more complex Factory Method implementation, giving you the best of both worlds: a simple entry point and a highly extensible backend.
Quiz: Test Your Knowledge
**Question:** What is the primary responsibility of a "Concrete Creator" in the Factory Method pattern?
a) To define the business logic that uses the product.
b) To define the interface for the products.
c) To implement the factory method and instantiate a specific concrete product.
d) To decide which concrete creator to use.
Answer
c) To implement the factory method and instantiate a specific concrete product. The abstract creator often contains the business logic, but the concrete creator's main role in the pattern is to handle the specific object creation.
Conclusion
The Factory Method pattern is an essential tool for any developer looking to write clean, maintainable, and extensible code. By delegating object creation to subclasses, you create a system that can easily accommodate new types of products without modifying existing code, perfectly adhering to the Open/Closed Principle.
While it introduces more classes than a Simple Factory, the long-term benefits in flexibility and maintainability are well worth the initial setup, especially in large and evolving applications.