策略模式(Strategy pattern),定义一系列算法,将每一种算法发封装起来可以相互替换使用,策略模式让算法独立于使用它的客户端而独立变化,也就是算法发生变化,不影响客户端的使用。
策略模式包含三个角色:
上下文(StrategyContext):包含一个Strategy的引用,通过它调用Strategy方法。
抽象策略(Strategy):一般为抽象类或接口。
具体策略类(ConcreteStrategy):策略的具体实现,一般为多个。
下面我们以商场会员打折为例:商场会员分为普通会员(不打折),银卡会员(打九折)和金卡会员(打八折)。源代码如下:
打折策略接口
public interface DiscountStrategy { double getDiscountPrice(double price);}
具体打折策略-普通会员
/** * 普通会员策略,不打折。 * * @author admin * */public class CommonDiscountStrategy implements DiscountStrategy { @Override public double getDiscountPrice(double price) { return price; }}
具体打折策略-银卡会员
/** * 银卡会员打九折 * * @author admin * */public class SilverDiscountStrategy implements DiscountStrategy { @Override public double getDiscountPrice(double price) { return price * 0.9; }}
具体打折策略-金卡会员
/** * 金卡会员打八折 * * @author admin * */public class GoldDiscountStrategy implements DiscountStrategy { @Override public double getDiscountPrice(double price) { return price * 0.8; }}
策略上下文
public class DiscountContext { private DiscountStrategy discountStrategy; public DiscountContext(DiscountStrategy discountStrategy) { this.discountStrategy = discountStrategy; } /** * 获取打折后的最终价格, * 该方法可以对计算结果进行格式化,比如加上单位、精确小数位等,是客户端更方便使用。 * * @param price * @return */ public String computePrice(double price) { double discountPrice = discountStrategy.getDiscountPrice(price); StringBuilder resultBuilder = new StringBuilder("¥") .append(discountPrice).append("元"); return resultBuilder.toString(); }}
Client
public class Client { public static void main(String[] args) { DiscountStrategy strategy = new GoldDiscountStrategy(); DiscountContext context = new DiscountContext(strategy); double originalPrice = 100.00; String discountPrice = context.computePrice(originalPrice); System.out.println("原价为" + originalPrice + ", 作为金卡会员,购物花费为:" + discountPrice); }}
运行结果:
原价为100.0, 作为金卡会员,购物花费为:¥80.0元
大功告成?且慢!在上述实现中,需要客户端自己去判断选择算法,能不能根据会员的类型,自动去选择呢?能不能增添或删除策略类,而不去修改客户端的方法呢?答案是工厂。我们将DiscountContext与工厂模式相结合,代码如下:
结合工厂之后的上下文:
public class DiscountContext2 { private DiscountStrategy discountStrategy; public DiscountContext2(String type) throws Exception { if (type.equals("普通")) { discountStrategy = new CommonDiscountStrategy(); } else if (type.equals("银卡")) { discountStrategy = new SilverDiscountStrategy(); } else if (type.equals("金卡")) { discountStrategy = new GoldDiscountStrategy(); } else { throw new Exception("会员类型不对"); } } /** * 获取打折后的最终价格, * 该方法可以对计算结果进行格式化,比如加上单位、精确小数位等,是客户端更方便使用。 * * @param price * @return */ public String computePrice(double price) { double discountPrice = discountStrategy.getDiscountPrice(price); StringBuilder resultBuilder = new StringBuilder("¥") .append(discountPrice).append("元"); return resultBuilder.toString(); }}
修改之后的客户端
public class Client2 { public static void main(String[] args) { String type = "金卡"; DiscountContext2 context = null; try { context = new DiscountContext2(type); } catch (Exception e) { // TODO Auto-generated catch block e.printStackTrace(); } double originalPrice = 100.00; String discountPrice = context.computePrice(originalPrice); System.out.println("原价为" + originalPrice + ", 作为" + type + "会员,购物花费为:" + discountPrice); }}
优化完毕,如果此时添加或者删除一个策略,我只需要修改策略上下文即可,对客户端无任何影响。
修改之后运行结果:
原价为100.0, 作为金卡会员,购物花费为:¥80.0元
上面实现了策略模式+工厂模式,能不能进一步优化呢?我们知道Spring是天然的工厂,当策略模式遇到了Spring,是不是更加美如画了呢?下面我们来实现一下。
策略的接口和各个具体策略类保持不变,将它们交给Spring容器管理。为了更加直观一些,在这里我们用xml配置方式,当然你也可以选择annotation的方式。
我们需要给策略上下文添加一个Map(Key为会员类型,Value为具体策略对象的一个引用),用于存放各个策略,这样我们可以很方便的根据会员类型获取相应的打折策略。当然我们需要事先将策略注入到Map中。
DiscountContextNew.java
public class DiscountContextNew { private DiscountStrategy discountStrategy; // 将打折策略注入到Map中。 MapstrategyMap = new HashMap (); /** * 获取打折后的最终价格, * 该方法可以对计算结果进行格式化,比如加上单位、精确小数位等,是客户端更方便使用。 * * @param price * @return * @throws Exception */ public String computePrice(String type, double price) throws Exception { // 获取策略 discountStrategy = strategyMap.get(type); if (discountStrategy == null) { throw new Exception("没有相对应的策略。"); } double discountPrice = discountStrategy.getDiscountPrice(price); StringBuilder resultBuilder = new StringBuilder("¥") .append(discountPrice).append("元"); return resultBuilder.toString(); } public Map getStrategyMap() { return strategyMap; } // 别忘了map的setter方法哦。 public void setStrategyMap(Map strategyMap) { this.strategyMap = strategyMap; } }
applicationContext.xml (将策略注入到Map中)
客户端需要一些变动,就是先根据Spring配置文件初始化spring容器、获得一个上下文,然后从spring容器中获取DiscountContextNew的实例,接下来就是计算价格了。
ClientNew.java
String type = "金卡";double originalPrice = 100.00; ApplicationContext ac = new ClassPathXmlApplicationContext("applicationContext.xml");DiscountContextNew discountContextNew = ac.getBean(DiscountContextNew.class); String discountPrice = null;try { discountPrice = discountContextNew.computePrice(type, originalPrice);} catch (Exception e) { // TODO Auto-generated catch block e.printStackTrace();}System.out.println("原价为" + originalPrice + ", 作为" + type + "会员,购物花费为:" + discountPrice);
如果此时再添加或者删除一个策略,我只需要修改Spring配置文件即可,对客户端无任何影响。
运行main方法,还是我们熟悉的结果:
原价为100.0, 作为金卡会员,购物花费为:¥80.0元
设计原则:
开闭原则:增加一个策略,只需要增加一个策略抽象类(或接口)的具体实现即可,对原有系统没有任何影响。
单一职责原则:没一种策略都在单独的类中,专注于自身的算法。
适用场景:
所有算法具备一些共有行为:本例中为计算打折后的价格。
所有算法都是平等的:因为平等,所以能自由切换。
运行时策略是唯一的。
总结
策略模式优点:避免了多个if-else分支,也避免了if-else深度嵌套(降低逻辑复杂度,UT写起来也方便了);降低客户端与具体业务的复杂的,符合开闭原则。
缺点:客户端需要自行选择策略(工厂模式解决了这一问题);每一种算法都需要一个具体类去实现,可能会导致程序中的对象过多。
友情提示:
源码地址(GitHub):
整个工程为Maven工程,对Maven不熟悉的同学可以查看我的另一篇博客