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.