Creational Design Patterns
Imagine you're designing a logistics management application. Your core module needs to create Transport objects to deliver goods. Initially, the company only uses trucks. So, you might write your code like this:
public class LogisticsService {
public void planDelivery(Cargo cargo) {
// ... some logic to plan the route
// The problem is here: direct coupling to a concrete class
Truck truck = new Truck();
truck.load(cargo);
truck.dispatch();
}
}
This code is tightly coupled. The LogisticsService knows explicitly about the Truck class. What happens when the business expands to use ships for overseas delivery? You would have to go back into the LogisticsService, add an if-else statement (if (type == 'land') new Truck() else if (type == 'sea') new Ship()), and modify its core logic.
This violates the Open/Closed Principle. The high-level LogisticsService is not closed for modification when a new transport type is introduced. How can we decouple the LogisticsService from the specific type of transport it needs to create?
Factory Method says: the parent class defines when an object is needed, but each child class decides what object to build.
Instead of calling new Truck() (or new Ship()) in the parent, the parent calls an overridable method—its factory method.
Every child overrides this method and returns the product it knows about.
Because creation happens in the child, you can add new products by adding new subclasses, without touching existing code. That’s why the pattern is sometimes called a “virtual constructor.”
The Factory Method pattern has four key components:
ITransport).Product interface (e.g., Truck, Ship).Product type. It contains the core business logic that relies on the Product, but it doesn't know which concrete product to create.Concrete Product.Let's refactor our LogisticsService to use the Factory Method pattern.
Step 1: Define the Product Interface This defines the contract for any type of transport.
public interface ITransport {
void load(Cargo cargo);
void dispatch();
}
// Concrete Products
public class Truck implements ITransport { /* ... implementation ... */ }
public class Ship implements ITransport { /* ... implementation ... */ }
Step 2: Create the Abstract Creator with the Factory Method
The Logistics creator class contains the high-level business logic. It knows it needs some kind of transport, but delegates the actual creation to its subclasses via the abstract createTransport() factory method.
public abstract class Logistics {
// This is the Factory Method. It's abstract, forcing subclasses to implement it.
public abstract ITransport createTransport();
// This is the core business logic. It is not coupled to any concrete transport type.
// It works with any object that satisfies the ITransport interface.
public void planDelivery(Cargo cargo) {
// ... some logic to plan the route
ITransport transport = createTransport(); // Call the factory method to get the object
transport.load(cargo);
transport.dispatch();
}
}
Step 3: Create Concrete Creators Each concrete creator knows which specific product to instantiate.
public class RoadLogistics extends Logistics {
@Override
public ITransport createTransport() {
// This creator is responsible for creating Trucks.
return new Truck();
}
}
public class SeaLogistics extends Logistics {
@Override
public ITransport createTransport() {
// This creator is responsible for creating Ships.
return new Ship();
}
}
Step 4: Using the Factory in the Client Code
Now, the client (e.g., the main application class) can decide which kind of logistics it needs at runtime. The client is coupled to the concrete creator, but the creator's core logic remains decoupled from the concrete product.
public class Application {
private static Logistics logistics;
public static void main(String[] args) {
// Read a configuration file, get user input, etc.
String transportType = "sea"; // Or "road"
if (transportType.equalsIgnoreCase("road")) {
logistics = new RoadLogistics();
} else if (transportType.equalsIgnoreCase("sea")) {
logistics = new SeaLogistics();
} else {
throw new IllegalArgumentException("Unknown transport type");
}
// The client code works with the abstract creator.
logistics.planDelivery(new Cargo());
}
}
Now, the client code can decide which kind of logistics it needs, and the correct transport will be created automatically, without the Logistics base class ever knowing the details.
A common source of confusion is the difference between the Factory Method pattern and a "Simple Factory" or "Static Factory". A Simple Factory is a single class with one method that has a large if-else or switch statement to create objects.
// This is NOT the Factory Method pattern. It's a "Simple Factory".
public class TransportFactory {
public static ITransport createTransport(String type) {
if ("truck".equalsIgnoreCase(type)) {
return new Truck();
} else if ("ship".equalsIgnoreCase(type)) {
return new Ship();
}
// If we add a Plane, we have to modify this method!
return null;
}
}
Key Differences:
Plane, you create a new AirLogistics class and you don't touch any existing code. In a Simple Factory, you must modify the central factory class, which violates the OCP.While a Simple Factory can be useful for centralizing object creation, the Factory Method is a true Gang of Four pattern that provides much greater flexibility and extensibility in complex systems. Knowing this distinction is a sign of a mature understanding of design patterns.
Benefits:
AirLogistics creator and Plane product without touching any of the existing Logistics, RoadLogistics, or SeaLogistics classes.Drawbacks:
planDelivery() acts as a Template Method: it defines the skeleton of the delivery algorithm. One of the steps in that algorithm, createTransport(), is the factory method that subclasses must implement.Logistics class does not depend on the low-level Truck or Ship classes. Instead, both depend on the ITransport abstraction, effectively "inverting" the dependency.