Friday, July 27, 2012

Springify The Strategy Pattern


            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
  1. Reference to the strategy pattern.
  2. Reference to the Spring Util schema documentation.


2 comments:

  1. Good synthetic article.
    I 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.

    ReplyDelete
  2. Thanks a lot. It was very helpful.

    ReplyDelete