The Factory Pattern is a widely-used creational design pattern in Java. However, the classic implementation has one major flaw — it violates the Open/Closed Principle from SOLID design. Every time a new type and implementation needs to be added the Factory class must be modified.
Enter Java 8, with its powerful lambda expressions and the Supplier<T> functional interface. These allow us to create a Dynamic Factory Pattern that is more flexible and extensible, keeping our factory class closed for modification and open for extension.
The Problem with Classic Factory Pattern
Let’s begin with a classic example.
interface Shape {
void draw();
}
class Circle implements Shape {
public void draw() {
System.out.println("Drawing a Circle");
}
}
class Square implements Shape {
public void draw() {
System.out.println("Drawing a Square");
}
}
class ShapeFactory {
public static Shape createShape(String type) {
switch (type) {
case "circle": return new Circle();
case "square": return new Square();
default: throw new IllegalArgumentException("Unknown shape: " + type);
}
}
}
Drawbacks
- Violation of the Open/Closed Principle: Every new shape requires modifying
ShapeFactory. - Hard to scale: Over time, the factory becomes a large, monolithic switch statement.
- Harder to test and maintain.
Dynamic Factory with Lambdas and Supplier
Java 8 introduced the Supplier<T> interface — a perfect fit for factories. It represents a function that supplies an instance of type T. Let’s build a Dynamic Factory that allows registration of new types at runtime, avoiding changes to the factory itself.
1. Define the Interface and Implementations
interface Shape {
void draw();
}
class Circle implements Shape {
public void draw() {
System.out.println("Drawing a Circle");
}
}
class Square implements Shape {
public void draw() {
System.out.println("Drawing a Square");
}
}2. Create the Dynamic Factory
import java.util.HashMap;
import java.util.Map;
import java.util.function.Supplier;
class DynamicShapeFactory {
private final Map<String, Supplier<Shape>> registry = new HashMap<>();
public void registerShape(String shapeName, Supplier<Shape> supplier) {
registry.put(shapeName.toLowerCase(), supplier);
}
public Shape createShape(String shapeName) {
Supplier<Shape> supplier = registry.get(shapeName.toLowerCase());
if (supplier != null) {
return supplier.get();
}
throw new IllegalArgumentException("Unknown shape: " + shapeName);
}
}3. Usage example
public class Main {
public static void main(String[] args) {
DynamicShapeFactory factory = new DynamicShapeFactory();
// Register shapes
factory.registerShape("circle", Circle::new);
factory.registerShape("square", Square::new);
// Create shapes dynamically
Shape shape1 = factory.createShape("circle");
Shape shape2 = factory.createShape("square");
shape1.draw(); // Output: Drawing a Circle
shape2.draw(); // Output: Drawing a Square
}
}Benefits of the Dynamic Factory Pattern
- Adheres to the Open/Closed Principle: New shapes can be added without modifying
DynamicShapeFactory. - Extensible at Runtime: You can load and register new types even via configuration files or dependency injection.
- Easier to Test: You can mock or stub factories by providing custom suppliers in test scenarios.
Conclusion
The Dynamic Factory Pattern in Java, enabled by lambdas and Supplier<T>, is a clean, elegant, and modern alternative to the classic factory approach. It supports better software architecture by reducing coupling, increasing flexibility, and embracing SOLID principles.
While it may introduce a slight upfront complexity due to registration logic, the long-term benefits — especially in scalable and modular applications — make it a powerful design choice.
