The Factory Design Pattern is a creational design pattern that helps decouple object creation from usage. But in real-world systems, we often ask:
"What if I need to add more types laterβwithout modifying the factory itself?"
In this post, you'll learn three extensible ways to implement the Factory Pattern in Java, starting from the classic version and evolving toward more open/closed, pluggable, and automatically discovered solutions.
π§ 1. Classic Factory (Not Extensible)
Letβs start with the traditional version where a factory creates shapes based on a string type.
π Shape.java
public interface Shape {
void draw();
}
π Circle.java
public class Circle implements Shape {
public void draw() {
System.out.println("Inside Circle::draw() method.");
}
}
π ShapeFactory.java
public class ShapeFactory {
public Shape getShape(String shapeType) {
if (shapeType.equalsIgnoreCase("CIRCLE")) return new Circle();
if (shapeType.equalsIgnoreCase("RECTANGLE")) return new Rectangle();
if (shapeType.equalsIgnoreCase("SQUARE")) return new Square();
return null;
}
}
β Limitation
- You must modify
ShapeFactory
to add a new shape. - Violates the Open/Closed Principle.
β 2. Registry-Based Factory (More Extensible)
To avoid modifying the factory, we'll create a registry that stores a mapping from shape name to constructor.
β Benefits
- No need to change the factory when adding new shapes.
- Shapes register themselves statically.
π Shape.java
public interface Shape {
void draw();
}
π ShapeFactory.java
import java.util.HashMap;
import java.util.Map;
import java.util.function.Supplier;
public class ShapeFactory {
private static final Map<String, Supplier<Shape>> registry = new HashMap<>();
public static void registerShape(String name, Supplier<Shape> supplier) {
registry.put(name.toLowerCase(), supplier);
}
public static Shape getShape(String name) {
Supplier<Shape> supplier = registry.get(name.toLowerCase());
if (supplier != null) return supplier.get();
throw new IllegalArgumentException("Unknown shape: " + name);
}
}
π Circle.java
public class Circle implements Shape {
static {
ShapeFactory.registerShape("circle", Circle::new);
}
public void draw() {
System.out.println("Inside Circle::draw() method.");
}
}
π Main.java
public class Main {
public static void main(String[] args) throws ClassNotFoundException {
Class.forName("Circle");
Shape circle = ShapeFactory.getShape("circle");
circle.draw();
}
}
π§ Note:
You must explicitly load each shape class (Class.forName(...)
) to trigger static registration.
π 3. Using Java ServiceLoader
(Auto Discovery)
Letβs automate shape discovery using Javaβs built-in ServiceLoader
. It reads implementations declared in META-INF/services
.
β Benefits
- Add new shapes by just implementing the interface and updating a config file.
- No manual registration or modification needed.
π Shape.java
public interface Shape {
void draw();
String getName();
}
π Circle.java
public class Circle implements Shape {
public void draw() {
System.out.println("Inside Circle::draw() method.");
}
public String getName() {
return "circle";
}
}
π META-INF/services/Shape
Create this file in resources
:
Circle
Rectangle
Square
Each line is a fully qualified class name of a shape.
π ShapeFactory.java
import java.util.*;
import java.util.function.Supplier;
public class ShapeFactory {
private static final Map<String, Supplier<Shape>> registry = new HashMap<>();
static {
ServiceLoader<Shape> loader = ServiceLoader.load(Shape.class);
for (Shape shape : loader) {
String name = shape.getName().toLowerCase();
registry.put(name, () -> {
try {
return shape.getClass().getDeclaredConstructor().newInstance();
} catch (Exception e) {
throw new RuntimeException("Could not instantiate shape: " + name, e);
}
});
}
}
public static Shape getShape(String name) {
Supplier<Shape> supplier = registry.get(name.toLowerCase());
if (supplier != null) return supplier.get();
throw new IllegalArgumentException("Shape not registered: " + name);
}
}
π Main.java
public class Main {
public static void main(String[] args) {
Shape shape = ShapeFactory.getShape("circle");
shape.draw();
}
}
β Summary Table
Approach | Extensible? | Auto-discovery | No Factory Changes? | Manual Setup Needed |
---|---|---|---|---|
Classic Factory | β No | β No | β No | β None |
Registry Pattern | β Yes | β No | β Yes | β Class.forName() |
ServiceLoader |
β Yes | β Yes | β Yes | β META-INF/services |
π§© Bonus: Plugin-Like Shape System
Using ServiceLoader
, your shape system behaves like a plugin architecture. You can even load new shapes from external jars at runtime, making this approach great for extensible frameworks and toolkits.
βοΈ Final Thoughts
If you're building a simple app, the classic factory works. But if youβre designing for extensibility, especially in a large codebase or plugin-like architecture, go for the ServiceLoader or Registry-Based Factory.
Let your factories scale without becoming a God-class.
Want the same pattern implemented in Spring or with third-party libraries like Reflections
? Let me know and Iβll walk you through that too.
Top comments (0)