The strategy pattern, sure we have heard enough and used enough
many times before. But if you are wondering how effectively one could use this
pattern in a spring powered application then here are few approaches. I
wouldn’t waste time talking about strategy pattern. So let’s get started with
some code, consider a typical eCommerce company offering various levels of
savings on shipping cost based on the type of memberships. So let’s keep this
logic simple here to try out various approaches.
First thing First
Here are few basic components of
strategy pattern first. Start with an interface ShippingCostStrategy.java as
shown below,
public interface IShippingCostStrategy { public double calculate(Item item); }
Now write few possible strategy implementations like below,
public class PrimeShippingCostStrategyImpl implements IShippingCostStrategy { public double calculate(Item item) { double cost = 0.0; //Calculate the shipping for prime members return cost; } }
public class PremiumShippingCostStrategyImpl implements IShippingCostStrategy { public double calculate(Item item) { double cost = 0.0; //Calculate the shipping for premium members return cost; } }
public class RegularShippingCostStrategyImpl implements IShippingCostStrategy { public double calculate(Item item) { double cost = 0.0; //Calculate the shipping for regular members return cost; } }
public class Item { private int itemId; private String code; private double price; public Item() { super(); } public Item(int itemId, String code, double price) { super(); this.itemId = itemId; this.code = code; this.price = price; } //All the getter and setter methods are omitted for brevity }
Define these beans in the shippingCost-context.xml like below and these are required no matter what the approach is,
<bean id="primeShippingStrategy" class=" xxx.xxx.PrimeShippingCostStrategyImpl" />
<bean id="premiumShippingStrategy" class=" xxx.xxx.PremiumShippingCostStrategyImpl" />
<bean id="regularShippingStrategy" class=" xxx.xxx.RegularShippingCostStrategyImpl" />
Approach #1
In this approach we will keep our context for the strategy
simple meaning we will let the client (service) to take the responsibility of determining
the possible right strategy implementation class and setting in to the context.
The calculateShipping method in the context will then call
the calculate method on the strategy that is being set.
The ShippingContext.java we got here is as shown below,
public class ShippingCostContext { IShippingCostStrategy shippingCostStrategy = null; public double calculateShipping(Item item){ return shippingCostStrategy.calculate(item); } public void setShippingCostStrategy(IShippingCostStrategy shippingCostStrategy) { this.shippingCostStrategy = shippingCostStrategy; } }
Let’s look at the ShippingCostService that is being the client for this strategy.
Here we are injecting all the strategy classes and context
into this client.
public class ShippingCostService { private PrimeShippingCostStrategyImpl primeShippingStrategy = null; private PremiumShippingCostStrategyImpl premiumShippingStrategy = null; private RegularShippingCostStrategyImpl regularShippingStrategy = null; private ShippingCostContext shippingContext = null; public double calculateShipping(Item item, String memberStatus){ double cost = 0.0; if(memberStatus.equalsIgnoreCase(StrategyConstants.MEMBER_PRIME)) shippingContext.setShippingCostStrategy(primeShippingStrategy); else if(memberStatus.equalsIgnoreCase(StrategyConstants.MEMBER_PREMIUM)) shippingContext.setShippingCostStrategy(premiumShippingStrategy); else if(memberStatus.equalsIgnoreCase(StrategyConstants.MEMBER_REGULAR)) shippingContext.setShippingCostStrategy(regularShippingStrategy); cost = shippingContext.calculateShipping(item); return cost; } //All the setter and getter for the above attributes goes here }//End of ShippingCostService
Now the spring configuration would be straight forward,
<bean id="shippingContext" class=" xxx.xxx.SpringShippingCostContext" />
<bean id="shippingCostService" class=" xxx.xxx.ShippingCostService" >
<property name="primeShippingStrategy" ref="primeShippingStrategy"/>
<property name="premiumShippingStrategy" ref="premiumShippingStrategy"/>
<property name="regularShippingStrategy" ref="regularShippingStrategy"/>
<property name="shippingContext" ref="shippingContext"/>
</bean>
Simple class to test this out,
public static void main(String args[]) { ApplicationContext context = new ClassPathXmlApplicationContext("shippingCost-Context.xml"); ShippingCostService shippingCostService = ShippingCostService)context.getBean("shippingCostService"); String memberStatus = StrategyConstants.MEMBER_PREMIUM; //REGULAR, PREMIUM, PRIME Item item = new Item(1, "PREMIUM", 20.00); double cost = shippingCostService.calculateShipping(item,memberStatus); System.out.println("Shipping cost: for Member type: "+ memberStatus+ ", cost: "+cost); }
Approach #2
In this approach we are
transferring the responsibility of determining the right strategy from client
to the context.
So we will have to inject all
the strategy implementation classes into the context as shown below,
public class ShippingCostContext { private PrimeShippingCostStrategyImpl primeShippingStrategy = null; private PremiumShippingCostStrategyImpl premiumShippingStrategy = null; private RegularShippingCostStrategyImpl regularShippingStrategy = null; public double calculateShipping(Item item, String memberStatus){ double cost = 0.0; if(memberStatus.equalsIgnoreCase(StrategyConstants.MEMBER_PRIME)) cost = primeShippingStrategy.calculate(item); else if(memberStatus.equalsIgnoreCase(StrategyConstants.MEMBER_PREMIUM)) cost = premiumShippingStrategy.calculate(item); else if(memberStatus.equalsIgnoreCase(StrategyConstants.MEMBER_REGULAR)) cost = regularShippingStrategy.calculate(item); return cost; } // All the setter and getter for the above attributes goes here }
As we see the calculateShipping method in context will
determine the call to the right strategy class based on the new parameter
memberStatus. There by we freed the ShippingCostService from making the
decision of injecting the right strategy implementation class into the context.
public class ShippingCostService { private ShippingCostContext shippingContext = null; public double calculateShipping(Item item, String memberStatus){ double cost = 0.0; cost = shippingContext.calculateShipping(item, memberStatus); return cost; } // All the setter and getter for the above attributes goes here }
The spring configuration for this approach would be,
<bean id="shippingContext" class=" xxx.xxx.SpringShippingCostContext" >
<property name="primeShippingStrategy" ref="primeShippingStrategy"/>
<property name="premiumShippingStrategy" ref="premiumShippingStrategy"/>
<property name="regularShippingStrategy" ref="regularShippingStrategy"/>
</bean>
<bean id="shippingCostService" class=" xxx.xxx.ShippingCostService" >
<property name="shippingContext" ref="shippingContext"/>
</bean>
We could test this approach out using the same test class
code as given above in approach 1.
Approach #3
The third
approach improvises the approach 2 by introducing a map of available strategies.
So introducing any new strategy would just be a matter of
configuring rather than coding in context.
Look at this code for context under this approach,
public class ShippingCostContext { private Map shippingStrategies = new HashMap(); public double calculateShipping(Item item, String memberstatus){ double cost = 0.0; if(shippingStrategies.containsKey(memberstatus)){ IShippingCostStrategy shippingCostStrategy = (IShippingCostStrategy)shippingStrategies.get(memberstatus); if(shippingCostStrategy != null) cost = shippingCostStrategy.calculate(item); } return cost; } public void setShippingStrategies(Map shippingStrategies) { this.shippingStrategies = shippingStrategies; } }
Context now holds the map of all configured strategy beans in it. This wouldn’t affect the way client invokes the strategy at all.
All it needs to do is pass an additional parameter
memberStatus to the context to help determine the right strategy.
public class ShippingCostService { private ShippingCostContext shippingContext = null; public double calculateShipping(Item item, String memberStatus){ double cost = 0.0; cost = shippingContext.calculateShipping(item, memberStatus); return cost; } }
The configuration for this approach is simple enough as we are going to use the util schema to achieve this.
We created the property shippingStrategies of type Map with
key using static fields and value would be respective bean.
<bean id="shippingContext" class="xxx.xxx.ShippingCostContext" >
<property name="shippingStrategies">
<map>
<entry>
<key>
<util:constant static-field="xxx.xxx.StrategyConstants.MEMBER_REGULAR"/>
</key>
<ref bean="regularShippingStrategy" />
</entry>
<entry>
<key>
<util:constant static-field=" xxx.xxx.StrategyConstants.MEMBER_PREMIUM"/>
</key>
<ref bean="premiumShippingStrategy" />
</entry>
<entry>
<key>
<util:constant static-field=" xxx.xxx.StrategyConstants.MEMBER_PRIME"/>
</key>
<ref bean="primeShippingStrategy" />
</entry>
</map>
</property>
</bean>
<bean id="shippingCostService" class=" xxx.xxx.ShippingCostService" >
<property name="shippingContext" ref="shippingContext"/>
</bean>
Make sure to add the util namespace and shema location information to the header beans of the context xml like as shown,
xmlns:util="http://www.springframework.org/schema/util"
xsi:schemaLocation="http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util-2.0.xsd"
That’s it you are ready to test your strategy using the same test class above.
Final Note
The advantage of approach 1 is, the
client will be in control if the business logic in client (service) that
determines the strategy to use is inseparable or tightly coupled. The same
advantage could turn into a disaster with tight coupling. So approach 2 would
bring in the much needed loose coupling and let strategy context take care of
determining the right strategy. This brings in an opportunity to add more
business intelligence into the context. The last approach as you saw definitely
improvises the overall strategy to implement the strategy pattern in spring. Sure
there must be even better approaches out there and if you do have one please
share.
Reference
- Reference to the strategy pattern.
- Reference to the Spring Util schema documentation.
Good synthetic article.
ReplyDeleteI personally prefer the third pattern, that avoids "if..else if..." and "switch...case", one of the goals of the Strategy pattern.
IMHO I see no actual benefit in separating Strategy context from the service implementation.
The service implementation itself could be the Context without drawbacks.
Thanks a lot. It was very helpful.
ReplyDelete