Making Factory Pattern Extensible using Java Reflections
Introduction
In the realm of software development, maintaining code that is both extensible and robust against frequent changes can be challenging. The Open/Closed Principle, a core tenet of SOLID design principles, states that software entities should be open for extension, but closed for modification. In this blog post, we'll explore a practical application of this principle in Java, focusing on a strategy pattern implementation used to handle various behaviors dynamically based on different "metrics". Initially, we'll see a basic implementation using a HashMap
and manual registrations, and then we'll enhance it using custom annotations and reflection to adhere more closely to the Open/Closed Principle.
Scenario
Imagine a Java application where different strategies need to be executed based on some runtime metrics. Each strategy is encapsulated in its class that implements an IStrategy
interface.
Initial Implementation: Using HashMap
Initially, our strategy factory uses a HashMap
to map metrics to specific strategy implementations. Here’s a basic outline:
public interface IStrategy {
void apply();
}
public class StrategyImplA implements IStrategy {
@Override
public void apply() {
System.out.println("Applying Strategy A");
}
}
public class StrategyImplB implements IStrategy {
@Override
public void apply() {
System.out.println("Applying Strategy B");
}
}
public class StrategyFactoryImpl implements IStrategyFactory {
private Map<String, IStrategy> strategies = new HashMap<>();
public StrategyFactoryImpl() {
strategies.put("metricA", new StrategyImplA());
strategies.put("metricB", new StrategyImplB());
}
@Override
public IStrategy getStrategy(String metric) {
IStrategy strategy = strategies.get(metric);
if (strategy != null) {
return strategy;
}
throw new IllegalArgumentException("No strategy found for metric: " + metric);
}
}
Drawback:
While this approach works, it violates the Open/Closed Principle. Every time a new strategy needs to be added, the StrategyFactoryImpl
must be modified by adding a new entry to the HashMap
. This constant modification is not ideal as it leads to more rigid and potentially error-prone code.
Enhanced Implementation: Using Annotations and Reflection
To adhere better to the Open/Closed Principle, we can use Java annotations and reflection. This approach allows new strategies to be added without modifying the factory code. Here's how we can implement it:
Step 1: Define a Custom Annotation
First, we define an annotation to specify the metric each strategy supports:
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface StrategyInfo {
String metric();
}
Step 2: Annotate Strategy Implementations
We then annotate each strategy implementation with the StrategyInfo
specifying the metric it handles:
@StrategyInfo(metric = "metricA")
public class StrategyImplA implements IStrategy { /* implementation omitted for brevity */ }
@StrategyInfo(metric = "metricB")
public class StrategyImplB implements IStrategy { /* implementation omitted for brevity */ }
Step 3: Modify the Factory to Use Reflection
The factory implementation is adjusted to automatically discover and instantiate strategies based on the annotation:
import org.reflections.Reflections;
import java.util.Set;
import java.util.HashMap;
import java.util.Map;
public class StrategyFactoryImpl implements IStrategyFactory {
private Map<String, IStrategy> strategies = new HashMap<>();
public StrategyFactoryImpl() {
Reflections reflections = new Reflections("your.package.name");
Set<Class<? extends IStrategy>> classes = reflections.getSubTypesOf(IStrategy.class);
for (Class<? extends IStrategy> clazz : classes) {
StrategyInfo info = clazz.getAnnotation(StrategyInfo.class);
if (info != null) {
try {
strategies.put(info.metric(), clazz.getDeclaredConstructor().newInstance());
} catch (Exception e) {
throw new RuntimeException("Failed to instantiate strategy for metric: " + info.metric(), e);
}
}
}
}
@Override
public IStrategy getStrategy(String metric) {
return strategies.get(metric);
}
}
Conclusion
By using custom annotations and reflection, we enhance the flexibility of our application, making it more adherent to the Open/Closed Principle. This method significantly simplifies adding new strategies by merely creating a new class and annotating it, without any changes required to the factory or other parts of the system. As developers strive to build scalable and maintainable applications, such techniques prove invaluable in managing complexity and promoting growth.
Refer this github repo for a different usecase which is runnable ->
https://github.com/VishalMCF/heartbeat-demo
Subscribe to my newsletter
Read articles from Vishal Yadav directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by