Design patterns are pivotal in crafting application solutions that are both maintainable and scalable. The Strategy Design Pattern is ideal for scenarios that require runtime selection among various available algorithms. In this blog, we’ll cover how to implement the Strategy Design Pattern in an AEM OSGi service, boosting your code’s flexibility and manageability.
What is the Strategy Design Pattern?
The Strategy Design Pattern comes under the category of behavioral design pattern which defines a family of algorithms, encapsulates each one, and makes them interchangeable. This pattern allows the algorithm to vary independently from the clients that use it. It’s particularly useful for scenarios where multiple methods can be applied to achieve a specific goal, and the method to be used can be selected at runtime.
You can read more about Strategy Pattern in general here.
UML Diagram of the Strategy Design Pattern
Here’s a UML diagram that illustrates the Strategy Design Pattern:
- Strategy: The interface that defines the common behavior for all strategies.
- ConreteImplementation1, ConreteImplementation2, ConreteImplementation3: Concrete implementations of the Strategy interface.
- Context: Maintains a reference to a Strategy object and uses it to execute the strategy.
Are There Use Cases in AEM
So, is there any scenario where we can use it in AEM? Yes! There are many AEM solution use cases where we have multiple strategies and the proper strategy is chosen at run time. We generally use conditional statements and switch statements in such scenarios. Here are a few examples where Strategy Pattern can be applied.
- Template Specific Behaviors: Different strategies for a component when it has different behavior according to different templates.
- Content Transformation: Different strategies for transforming HTML, JSON, or XML content.
- Brand-specific behaviors: Different strategies for individual brand sites in multitenant environments when they share certain common code base and scenarios.
- Channel Specific implementations: Different channels for sending notifications such as email, and web.
Benefits of Using the Strategy Design Pattern
Implementing the Strategy Design Pattern in AEM OSGi services offers numerous benefits, aligning well with SOLID principles and other best practices in software development.
Six Benefits
- Adaptability and Scalability: The Strategy Design Pattern facilitates the addition of new algorithms without altering the existing code, enhancing the system’s extensibility. When a new algorithm is required, it can be seamlessly integrated by creating a new strategy class, thereby leaving the context and other strategies unchanged.
- Adherence to SOLID Principles: The Strategy Design Pattern aligns with SOLID principles by ensuring each strategy class adheres to the Single Responsibility Principle (SRP), encapsulating a specific algorithm for easier comprehension, testing, and maintenance. It supports the Open/Closed Principle (OCP) by allowing new strategies to be added without modifying existing code. The pattern ensures compliance with the Liskov Substitution Principle (LSP), enabling interchangeable use of strategy implementations without altering functionality. It adheres to the Interface Segregation Principle (ISP) by having clients interact with the strategy interface, thus reducing unnecessary dependencies. Lastly, it upholds the Dependency Inversion Principle (DIP) by making the context depend on abstractions rather than concrete implementations, promoting flexible and decoupled code.
- Ease of Testing: Since each strategy is a separate implementation and does not directly depend on other implementations, it can be unit-tested independently of other strategies and the context. This leads to more manageable and comprehensible tests.
- Code Reusability: If needed Strategies can be reused across different parts of the application or even in different projects. This reduces redundancy and encourages the reuse of well-tested code.
- Maintainability: The clear separation of different algorithms into their own classes means that any changes or additions to the algorithms do not impact the rest of the codebase. This makes the system more maintainable.
- Better Code Readability: By encapsulating the logic of each algorithm in its own implementation class, the Strategy Design Pattern makes the code more readable. Developers can quickly understand the purpose of each strategy by looking at the strategy interface and its implementations.
Typical Strategy Implementation for AEM
In AEM, we can utilize regular classes for strategy implementation, but with OSGi, we get a better experience of handling strategies. A typical implementation of the Strategy Design Pattern in AEM will consist of:
- Strategy Context: Holds all the strategies for the client.
- Strategy Interface: Provides a generic interface for different strategies.
- OSGi Implementations for Strategies: Contains the logic for individual algorithms for each strategy.
Implementation Steps
Step 1: Define the Strategy Interface
First, create a Java interface that defines the common behavior for all strategies.
public interface StrategyService { /** * Name of the strategy. * @return Strategy name. */ String getName(); /** * Executes the strategy based on criteria. * @param strategyBean Dummy Strategy bean object consists of the fields required for strategy execution. * @return Strategy Result bean. You can replace it with result object according to your need. */ StrategyResult execute(StrategyBean strategyBean); /** * Method for executing isFit operation if given strategy is fit for scenario. * @param strategyBean Strategy Object consisting of params needed for strategy is fit operation. * @return true if given strategy fits the criteria. */ boolean isFit(StrategyBean strategyBean); }
Comments are self-explanatory here but let’s still deep dive into what each method intends to do.
String getName()
Name of the strategy. The name should be self-explanatory. So, when someone sneaks into the code, they should come to know for what scenario a strategy is aiming for.
StrategyResult execute(StrategyBean strategyBean);
This method executes the logic for the operation required at runtime. StrategyResult Bean represented here is just for reference. You can update the same according to your operation.
StrategyBean consists of params needed for executing the operation and again this is also just for reference you can execute the same with your own object.
boolean isFit(StrategyBean strategyBean);
This method is where decisions will be made if the given strategy is fit for the scenario or not. StrategyBean will provide the necessary parameters to execute the operation.
Step 2: Implement Concrete Strategies
Create multiple implementations of the StrategyService interface. Each class will implement the methods differently.
Here for reference, let’s create two concrete implementations for StrategyService namely FirstStrategyServiceImpl and SecondStrategyServiceImpl
@Component(service = StrategyService.class, immediate = true, property = { "strategy.type=FirstStrategy" }) @Slf4j public class FirstStrategyServiceImpl implements StrategyService { @Override public String getName() { return "FirstStrategy"; } @Override public StrategyResult execute(StrategyBean strategyBean) { log.info("Executing First Strategy Service Implementation...."); //Implement Logic return new StrategyResult(); } @Override public boolean isFit(StrategyBean strategyBean) { //Implement Logic return new Random().nextBoolean(); } }
And our Second Service implementation here will look like this:
@Component(service = StrategyService.class, immediate = true, property = { "strategy.type=SecondStrategy" }) @Slf4j public class SecondStrategyServiceImpl implements StrategyService { @Override public String getName() { return "SecondStrategy"; } @Override public StrategyResult execute(StrategyBean strategyBean) { log.info("Executing Second Strategy Service Implementation...."); //Implement Logic return new StrategyResult(); } @Override public boolean isFit(StrategyBean strategyBean) { //Implement Logic return new Random().nextBoolean(); } }
Here, we do not have any logic. We’re just giving dummy implementations. You can add your operational logic in execute and isFit method.
Step 3: Implement Context Service Class
Our context service implementation will hold all the strategies with dynamic binding and will look like this.
@Component(service = StrategyContextService.class, immediate = true) @Slf4j public class StrategyContextServiceImpl implements StrategyContextService { private final Map<String, StrategyService> strategies = new HashMap<>(); @Reference(cardinality = ReferenceCardinality.MULTIPLE, policy = ReferencePolicy.DYNAMIC) protected void bindStrategy(final StrategyService strategy) { strategies.put(strategy.getName(), strategy); } protected void unbindStrategy(final StrategyService strategy) { strategies.remove(strategy.getName()); } public Map<String, StrategyService> getStrategies() { return strategies; } @Override public StrategyService getApplicableStrategy(final StrategyBean strategyBean) { return strategies .values() .stream() .filter(strategyService -> strategyService.isFit(strategyBean)) .findFirst() .orElse(null); } @Override public StrategyResult executeApplicableStrategy(final StrategyBean strategyBean) { final var strategyService = getApplicableStrategy(strategyBean); if (strategyService != null) { return strategyService.execute(strategyBean); } else { // Run Default Strategy Or Update Logic Accordingly return strategies.get("default").execute(strategyBean); } } @Override public Collection<StrategyService> getAvailableStrategies() { return strategies.values(); } @Override public Map<String, StrategyService> getAvailableStrategiesMap() { return strategies; } }
Here you can see we are dynamically binding instances of StrategyService implementations. So, we don’t need to worry about change in this context class whenever new Strategy is implemented. The context here will dynamically bind the same and update available strategies.
We are providing methods here to get the strategy, execute the strategies, and get all the available strategies.
The advantage we have is we don’t need to worry about changes needed for additional strategies. When a new strategy is needed, we just need its implementation class and don’t need to worry about its impact on others. If isFit logic is not causing conflict with others we don’t need to worry about other services. This isolation gives us confidence for complex solutions as we have the flexibility here to not worry about the impact of newly added functionalities.
Step 4: Using this implementation
Since this is OSGi implementation you can use this in any OSGi Services, Servlet, Workflows, Schedulers, Sling Jobs, Sling Models, etc.
You can either use this directly or you can create a wrapper service around this if any further post-processing of strategy results is needed.
Our example consumer service and sling model will look like this:
@Component(service = StrategyConsumer.class, immediate = true) public class StrategyConsumer { @Reference private StrategyContextService strategyContext; public StrategyResult performTask() { final StrategyBean strategyBean = new StrategyBean(); //Set strategy object based on data StrategyResult strategyResult = strategyContext.executeApplicableStrategy(strategyBean); //Do Post Processing return strategyResult; } public StrategyResult performTask(final StrategyBean strategyBean) { StrategyResult strategyResult = strategyContext.executeApplicableStrategy(strategyBean); //Do Post Processing return strategyResult; } }
The method can be updated to accommodate logical operation for the addition of data into strategy bean if needed.
Sample sling model which will use consumer service or directly connect with context service will look like this:
@Model(adaptables = SlingHttpServletRequest.class) public class StrategyConsumerModel { @OSGiService private StrategyConsumer strategyConsumer; @PostConstruct public void init(){ StrategyResult strategyResult = strategyConsumer.performTask(); //do processing if needed // OR StrategyBean strategyBean = new StrategyBean(); strategyResult = strategyConsumer.performTask(strategyBean); } }
And you are all set now.
This was just a dummy implementation, but you can twist it according to your business needs.
In the introduction, use cases have already been mentioned. You can use this in implementing such scenarios. Additional use cases can be any task where you need to select implementation dynamically at run time based on scenarios.
Final Thoughts
With the above implementation, we can see that The Strategy Design pattern is well suited for managing and extending complex algorithms in a clean and modular way which also adheres to best practices including SOLID principles. It promotes flexibility, scalability, maintainability, and ease of testing. As you develop AEM solutions consider incorporating this, it will significantly improve code quality and reusability ensuring that evolving business requirements can be accommodated easily.
Learn more about AEM developer tips and tricks by reading our Adobe blog!