Have you ever found yourself tangled in a web of if-else if-else statements just to create an object? This common scenario, where client code is burdened with the logic of which concrete class to instantiate, is a classic sign of tightly coupled code. It’s a maintenance nightmare that violates core software design principles.
The Simple Factory Pattern offers a clean and straightforward solution. It’s a creational pattern that introduces a centralized class responsible for instantiating objects, effectively hiding the complex creation logic from the client.
[!NOTE] The Simple Factory is a creational design pattern that provides a centralized interface for creating objects of a common superclass, letting the factory decide which subclass to instantiate based on input parameters. It is technically considered a programming idiom rather than a formal Gang of Four (GoF) pattern, but it’s an essential stepping stone.
The Problem: A Growing Logistics System
Imagine we’re building a logistics application. Initially, the business requires two methods of transport: by Truck and by Ship.
First, we define a common interface that all our transport types will implement.
// src/main/java/com/example/transport/Transport.java
package com.example.transport;
/**
* A common interface for all transport types.
*/
public interface Transport {
String deliver();
}
Next, we create our concrete implementations for Truck and Ship.
// src/main/java/com/example/transport/Truck.java
package com.example.transport;
public class Truck implements Transport {
@Override
public String deliver() {
return "Delivering cargo by land in a truck.";
}
}
// src/main/java/com/example/transport/Ship.java
package com.example.transport;
public class Ship implements Transport {
@Override
public String deliver() {
return "Delivering cargo by sea in a container ship.";
}
}
Without a factory, the client code that needs a transport object is forced to handle the instantiation logic itself.
// src/main/java/com/example/Main.java
public class Main {
public static void main(String[] args) {
String transportType = "Ship"; // This could come from user input, config, etc.
Transport transport;
if (transportType.equalsIgnoreCase("Truck")) {
transport = new Truck();
} else if (transportType.equalsIgnoreCase("Ship")) {
transport = new Ship();
} else {
throw new IllegalArgumentException("Unknown transport type");
}
System.out.println(transport.deliver());
// Output: Delivering cargo by sea in a container ship.
}
}
This might seem acceptable at first, but what happens when the business requirements change?
The Breaking Point: Adding a New Transport Type
The business now wants to add Airplanes to the logistics network. To do this, we must:
- Create a new
Airplaneclass that implements theTransportinterface. - Modify the client’s
if-elseblock everywhere it’s used in the application.
Here’s the new Airplane class:
// src/main/java/com/example/transport/Airplane.java
package com.example.transport;
public class Airplane implements Transport {
@Override
public String deliver() {
return "Delivering cargo by air in a cargo plane.";
}
}
And here is the painful change we must now make to our client code:
// src/main/java/com/example/Main.java
public class Main {
public static void main(String[] args) {
String transportType = "Airplane";
Transport transport;
if (transportType.equalsIgnoreCase("Truck")) {
transport = new Truck();
} else if (transportType.equalsIgnoreCase("Ship")) {
transport = new Ship();
- } else {
- throw new IllegalArgumentException("Unknown transport type");
- }
+ } else if (transportType.equalsIgnoreCase("Airplane")) {
+ transport = new Airplane();
+ } else {
+ throw new IllegalArgumentException("Unknown transport type");
+ }
System.out.println(transport.deliver());
}
}
[!WARNING] This approach has several major flaws:
- Violation of the Single Responsibility Principle (SRP): The client is now responsible for both its primary job and the logic of object creation.
- Violation of the Open/Closed Principle (OCP): To add a new transport type, we have to modify existing, working client code, which is risky.
- Code Duplication: If this logic is needed in multiple places, we have to find and update every single
if-elseblock, which is error-prone.
This is where the Simple Factory comes to the rescue.
The Solution: Centralizing Creation with a Simple Factory
We’ll create a TransportFactory class. Its sole purpose is to create transport objects. This centralizes the creation logic, decoupling the client from the concrete implementations.
First, let’s visualize our new project structure.
logistics-app/
└── src/
└── main/
└── java/
└── com/
└── example/
├── transport/
│ ├── Transport.java # Interface
│ ├── Truck.java
│ ├── Ship.java
│ └── Airplane.java
├── factory/
│ └── TransportFactory.java # Our new factory
└── Main.java # The client
The factory itself is a simple class with a static method.
// src/main/java/com/example/factory/TransportFactory.java
package com.example.factory;
import com.example.transport.*;
public class TransportFactory {
/**
* Creates a Transport object based on the given type.
* @param type The type of transport to create.
* @return A new instance of a Transport implementation.
*/
public static Transport createTransport(String type) {
if (type == null || type.isEmpty()) {
return null;
}
switch (type.toLowerCase()) {
case "truck":
return new Truck();
case "ship":
return new Ship();
case "airplane":
return new Airplane();
default:
throw new IllegalArgumentException("Unknown transport type: " + type);
}
}
}
This class diagram shows the new relationships. The Main client now only knows about the TransportFactory and the Transport interface, not the concrete classes.
classDiagram
class Main {
<<Client>>
}
class TransportFactory {
<<static>> createTransport(String type) Transport
}
class Transport {
<<Interface>>
deliver()
}
class Truck {
deliver()
}
class Ship {
deliver()
}
class Airplane {
deliver()
}
Main ..> TransportFactory : uses
Main ..> Transport : uses
TransportFactory ..> Truck : creates
TransportFactory ..> Ship : creates
TransportFactory ..> Airplane : creates
Truck --|> Transport
Ship --|> Transport
Airplane --|> Transport
Refactoring the Client to Use the Factory
Now, we can dramatically simplify our client code. The messy if-else block is replaced with a single, clean call to the factory.
// src/main/java/com/example/Main.java
+ import com.example.factory.TransportFactory;
+ import com.example.transport.Transport;
public class Main {
public static void main(String[] args) {
String transportType = "Airplane";
- Transport transport;
-
- if (transportType.equalsIgnoreCase("Truck")) {
- transport = new Truck();
- } else if (transportType.equalsIgnoreCase("Ship")) {
- transport = new Ship();
- } else if (transportType.equalsIgnoreCase("Airplane")) {
- transport = new Airplane();
- } else {
- throw new IllegalArgumentException("Unknown transport type");
- }
+ Transport transport = TransportFactory.createTransport(transportType);
System.out.println(transport.deliver());
// Output: Delivering cargo by air in a cargo plane.
}
}
Look at how clean that is! If we need to add a Drone transport type later, we only need to create the Drone class and add one line to the TransportFactory. No client code needs to be touched.
Benefits of the Simple Factory Pattern
The advantages of this approach are significant and immediately apparent.
mindmap
root((Simple Factory Benefits))
Decoupling
": Client depends on the interface, not concrete classes."
Centralized Control
": Object creation logic is in one place."
": SRP is upheld."
Improved Maintainability
": Adding new types only requires changing the factory."
": Reduces risk of bugs."
Enhanced Readability
": Client code becomes cleaner and more declarative."
Deep Dive: Best Practices & Edge Cases
### When NOT to Use a Simple Factory The transcript makes an excellent point: if your object creation logic is used in only **one** place and is unlikely to change, a Simple Factory might be overkill. Patterns are tools to solve problems, not rules to be followed blindly. Always evaluate if the added complexity is justified. ### Best Practice: Using Enums for Type Safety Using raw strings for types is fragile and prone to typos. A much safer and more robust approach is to use an `enum`. 1. **Create a `TransportType` enum:** ```java // src/main/java/com/example/transport/TransportType.java package com.example.transport; public enum TransportType { TRUCK, SHIP, AIRPLANE } ``` 2. **Update the Factory:** ```java // src/main/java/com/example/factory/TransportFactory.java public static Transport createTransport(TransportType type) { // No need for null checks if the enum is used correctly switch (type) { case TRUCK: return new Truck(); case SHIP: return new Ship(); case AIRPLANE: return new Airplane(); default: // This case becomes almost unreachable if using enums throw new IllegalArgumentException("Unsupported transport type: " + type); } } ``` 3. **Update the Client:** ```java // src/main/java/com/example/Main.java import com.example.transport.TransportType; // ... Transport transport = TransportFactory.createTransport(TransportType.AIRPLANE); System.out.println(transport.deliver()); ``` This prevents runtime errors from typos like `"air-plane"` and provides compile-time safety.Test Your Knowledge
Quiz: What is the primary principle that the Simple Factory pattern helps to enforce?
A) DRY (Don't Repeat Yourself)
B) SRP (Single Responsibility Principle)
C) YAGNI (You Ain't Gonna Need It)
D) Both A and B
Show Solution
Answer: D) Both A and B
The Simple Factory helps enforce the Single Responsibility Principle (SRP) by moving the responsibility of object creation out of the client and into a dedicated factory. It also promotes the Don't Repeat Yourself (DRY) principle by ensuring that the logic for instantiation exists in only one place, rather than being duplicated across multiple clients.
Conclusion
The Simple Factory is an essential idiom in any developer’s toolkit. By centralizing object creation, it decouples client code from concrete implementations, making your application more robust, maintainable, and easier to extend. While not a formal “GoF” pattern, it lays the foundation for understanding more advanced creational patterns like the Factory Method and Abstract Factory.