假设现在要设计一个麦各类书籍的电子商务汪涵的(Shoping Card)系统,一个最简单的情况就是把所有货品的单价乘上数量,但是实际情况肯定要比这复杂。比如本网站可能对所有的教材类图书实行每本两元的折扣;对连环画类图书提供每本10%的促销折扣,而非教材类的计算机图书有5%的折扣;对其余书没有折扣。由于有这样复杂的折扣算法,使得价格计算问题需要系统地解决。
那么怎么样才能解决这个问题呢?
其实,解决方法不止一种,例如我们可以把所有逻辑放在客户端利用条件语句判断决定使用哪一种算法;也可以利用继承在子类里面实现不同打折算法;还可以利用策略模式将环境和各种算法分开,将具体实现与客户端解耦。
实现这个策略的UML图如下:
抽象策略类(DiscountStrategy)
package com.strategy.booksale; /** * 抽象策略类,定义了抽象算法 * @author LLS * */ abstract public class DiscountStrategy { //抽象方法 abstract public double calculateDiscount(); }
10%的折扣促销类(PercentageStrategy)
package com.strategy.booksale; /** * 折扣销售图书类 * @author LLS * */ public class PercentageStrategy extends DiscountStrategy { //保存单价、数量、总额 private double percent = 0.0; private double price = 0.0; private int copies = 0; public PercentageStrategy(double price, int copies,double percent) { this.percent=percent; this.price = price; this.copies = copies; } public double getPercent() { return percent; } public void setPercent(double percent) { this.percent = percent; } //覆盖父类的抽象方法 public double calculateDiscount() { return copies * price * percent; } }
平价打折类(FlatRateStrategy)
package com.strategy.booksale; /** * 平价销售图书,不进行打折算法类 * @author LLS * */ public class FlatRateStrategy extends DiscountStrategy { //保存图书单价、数量、总额 private double amount; private double price = 0; private int copies = 0; public FlatRateStrategy(double price, int copies) { this.price = price; this.copies = copies; } public double getAmount() { return amount; } public void setAmount(double amount) { this.amount = amount; } //覆盖了抽象类的方法 public double calculateDiscount() { return copies * amount; } }
不打折类(NoDiscountStrategy)
package com.strategy.booksale;
public class NoDiscountStrategy extends DiscountStrategy
{
private double price = 0.0;
private int copies = 0;
public NoDiscountStrategy(double price, int copies)
{
this.price = price;
this.copies = copies;
}
public double calculateDiscount()
{
return price*copies;
}
}
维护抽象类的引用
package com.strategy.booksale;
/**
* 维护抽象策略类的引用
* @author LLS
*
*/
public class Context {
//维护策略抽象类的一个引用
DiscountStrategy discountStrategy;
//传入具体策略对象
public Context(DiscountStrategy discountStrategy)
{
this.discountStrategy=discountStrategy;
}
//根据具体策略对象调用其方法
public void ContextInterface()
{
discountStrategy.calculateDiscount();
}
}
客户端测试类(Test)
package com.strategy.booksale; public class Test { public static void main(String[] args) { //维护抽象策略类 Context context; //采用平价打折,单价为10元,数量5本 context=new Context(new FlatRateStrategy(10, 5)); //采用百分比打折10% context=new Context(new PercentageStrategy(10, 5,0.1)); } }
这样利用策略模式已经解决了多种打折的问题,但是你有没有发现策略只是实现了在不同打折方法或不同算法行为之间得灵活切换,并没有控制实例化哪一个算法,需要用哪一个优惠方式是由客户端决定的,所以客户端与具体的实现类之间耦合性很大,还需要进一步解耦。
策略模式更注重于n选1的情况,这也就是说如果我想组合几种不同的打折策略,策略就会显得不恰当,因为你需要将多个打折方法都写到一个类里面去,为解决这种情况,可以配合装饰(Decorator)模式一起使用。
装饰模式适合给一个类添加额外的职责,并且对客户端透明。
我们来看一张表示装饰模式的图,这张图即表明了它的添加功能特性也它的透明性。
大家很容器想到简单工厂,它就是一个封装产生对象的过程的类,通过传入字符串的方式决定实例化哪一个类,但是它也有不足,如果我们需要加入新的打折策略时,就需要改动工厂里面的代码,这违反了OCP原则。
我们可以利用反射来动态决定实例化哪个策略,配置文件和反射类如下:
配置文件设置
<?xml version="1.0" encoding="UTF-8"?>
<config>
<!-- 标签 -->
<strategy-class>
<!-- 折扣策略类 -->
<strategy id="com.strategy.booksale.DiscountStrategy" class="com.strategy.booksale.PercentateStrategy"></strategy>
</strategy-class>
</config>
反射类
package com.strategy.booksale;
import java.util.HashMap;
import java.util.Map;
import org.dom4j.Document;
import org.dom4j.Element;
import org.dom4j.io.SAXReader;
/**
* 反射类
* @author LLS
*
*/
public class Reflection{
//采用饿汉式单利模式,可能占内存
private static Reflection instance=new Reflection();
//系统缺省配置文件名称
private final String sysconfig="sys-config.xml";
//保存具体策略键值对
private Map strategyMap =new HashMap();
//读取出来的document对象
private Document doc;
private Reflection()
{
try {
doc=new SAXReader().read(Thread.currentThread().getContextClassLoader().getResourceAsStream(beansConfigFile));
} catch (Exception e) {
e.printStackTrace();
}
}
//得到实例 的方法
public static Reflection getInstance() {
return instance;
}
/**
* 根据策略 编号,取得的具体策略
* @param beanId
* @return
*/
public synchronized Object getStrategyObject(Class c)
{
//判断serviceMap里面是否有service对象,没有创建,有返回
if (strategyMap.containsKey(c.getName())) {
return strategyMap.get(c.getName());
}
//返回指定ID的Element对象
Element strategyElement=(Element)doc.selectSingleNode("//strategy[@id=\""+c.getName()+"\"]");
String className=strategyElement.attributeValue("class");
Object strategyObject=null;
try {
strategyObject = Class.forName(className).newInstance();
strategyMap.put(c.getName(), strategyObject);
} catch (Exception e) {
e.printStackTrace();
throw new RuntimeException();
}
return strategyObject;
}
}
改写后的客户端如下:
package com.strategy.booksale;
public class Test {
public static void main(String[] args)
{
//维护抽象策略类
Context context;
//利用反射动态决定使用哪一种打折策略
DiscountStrategy discountStrategy=(DiscountStrategy)Reflection.getInstance().getStrategyObject(DiscountStrategy.class);
//客户端只需要识别策略抽象类即可,与具体的算法解耦
context=new Context(discountStrategy);
}
}
这样一来客户端完全不知道有什么算法,也不知道该实例化哪一个,减少了客户端的职责。
最后,我们用装饰模式来解决策略不可以组合多个打折方式的不足,装饰模式的主要作用即可以给一个对象动态添加多种功能,下面是我画的类图,有了类图代码可以自己实现,让它们共同实现了同一个抽象类。
左边部分是装饰模式负责动态组合各种打折方法,右边是策略模式动态选择其中一种打折策略,之所以它们的功能可以一起使用,这里是因为他们实现了一个共同的接口 (Interface)。
有些问题如果我们站在接口或抽象类的角度去考虑,用接口和抽象的方式去思考,有时问题会容易解决一些,而不要一直在某个具体的类范围内考虑问题,即把思考的角度范围扩展,从高层次考虑更容容易解决下面层次的问题。
思想上移一些,站在这个问题的更高一层。