写在前
在实际的项目开发中,策略模式也比较常用。最常见的应用场景是,利用它来避免冗长的 if-else 或 switch 分支判断。不过,它的作用还不止如此。它也可以像模板模式那样,提供框架的扩展点等等。
工厂模式是解耦对象的创建和使用,观察者模式是解耦观察者和被观察者。策略模式跟两者类似,也能起到解耦的作用,不过,策略模式解耦的是策略的定义、创建、使用这三部分。接下来,我就详细讲讲一个完整的策略模式应该包含的这三个部分。
1.策略的定义
策略类的定义比较简单,包含一个策略接口和一组实现这个接口的策略类。因为所有的策略类都实现相同的接口,所以,客户端代码基于接口而非实现编程,可以灵活地替换不同的策略。示例代码如下所示:
// 抽象策略 public interface Strategy { void algorithmInterface(); } // 具体策略A public class ConcreteStrategyA implements Strategy { @Override public void algorithmInterface() { //具体的算法... } } // 具体策略B public class ConcreteStrategyB implements Strategy { @Override public void algorithmInterface() { //具体的算法... } }
2.策略的创建
因为策略模式会包含一组策略,在使用它们的时候,一般会通过类型(type)来判断创建哪个策略来使用。为了封装创建逻辑,我们需要对客户端代码屏蔽创建细节。我们可以把根据 type 创建策略的逻辑抽离出来,放到工厂类中。示例代码如下所示:
// 策略工厂类 public class StrategyFactory { private static final MapString, Strategy strategies = new HashMap(); static { strategies.put("A", new ConcreteStrategyA()); strategies.put("B", new ConcreteStrategyB()); } public static Strategy getStrategy(String type) { if (type == null || type.isEmpty()) { throw new IllegalArgumentException("type should not be empty."); } return strategies.get(type); } }
3.策略的使用
策略模式的参与者:
- Strategy:定义了所支持算法的公共接口,各种不同的算法以不同的方式实现这个接口,Context通过这个接口来调用ConcreteStrategy定义的算法。一般使用接口或抽象类实现。
- ConcreteStrategy:继承抽象策略(Strategy)角色,封装了具体的算法和行为。
- Context:策略的外部封装类,或者说策略的容器类。它维护了一个对Strategy对象的引用。负责根据不同策略动态设置运行时Strategy具体的ConcreteStrategy,并实现交互和数据传递。
我们知道,策略模式包含一组可选策略,客户端代码一般如何确定使用哪个策略呢?最常见的是运行时动态确定使用哪种策略,这也是策略模式最典型的应用场景。
这里的“运行时动态”指的是,我们事先并不知道会使用哪个策略,而是在程序运行期间,根据配置、用户输入、计算结果等这些不确定因素,动态决定使用哪种策略。接下来,我们通过一个例子来解释一下。
// 策略接口:EvictionStrategy // 策略类:LruEvictionStrategy、FifoEvictionStrategy、LfuEvictionStrategy... // 策略工厂:EvictionStrategyFactory public class UserCache { private MapString, User cacheData = new HashMap(); private EvictionStrategy eviction; public UserCache(EvictionStrategy eviction) { this.eviction = eviction; } //... } // **Context**:运行时动态确定,根据配置文件的配置决定使用哪种策略 public class Application { public static void main(String[] args) throws Exception { EvictionStrategy evictionStrategy = null; Properties props = new Properties(); props.load(new FileInputStream("./config.properties")); String type = props.getProperty("eviction_type"); evictionStrategy = EvictionStrategyFactory.getEvictionStrategy(type); UserCache userCache = new UserCache(evictionStrategy); //... } }
策略模式可以省去if-else,这得益于策略工厂类。在工厂类中,我们用 Map 来缓存策略,根据 type 直接从 Map 中获取对应的策略,从而避免 if-else 分支判断逻辑。等后面讲到使用状态模式来避免分支判断逻辑的时候,你会发现,它们使用的是同样的套路。本质上都是借助“查表法”,根据 type 查表(代码中的 strategies(策略工厂类) 就是表)替代根据 type 分支判断。
对于 Java 语言来说,我们可以通过反射来避免对策略工厂类的修改。具体是这么做的:我们通过一个配置文件或者自定义的 annotation 来标注都有哪些策略类;策略工厂类读取配置文件或者搜索被 annotation 标注的策略类,然后通过反射动态地加载这些策略类、创建策略对象;当我们新添加一个策略的时候,只需要将这个新添加的策略类添加到配置文件或者用 annotation 标注即可。
一提到 if-else 分支判断,有人就觉得它是烂代码。如果 if-else 分支判断不复杂、代码不多,这并没有任何问题,毕竟 if-else 分支判断几乎是所有编程语言都会提供的语法,存在即有理由。遵循 KISS 原则,怎么简单怎么来,就是最好的设计。非得用策略模式,搞出 n 多类,反倒是一种过度设计。
一提到策略模式,有人就觉得,它的作用是避免 if-else 分支判断逻辑。实际上,这种认识是很片面的。策略模式主要的作用还是解耦策略的定义、创建和使用,控制代码的复杂度,让每个部分都不至于过于复杂、代码量过多。除此之外,对于复杂代码来说,策略模式还能让其满足开闭原则,添加新策略的时候,最小化、集中化代码改动,减少引入 bug 的风险。
实际上,设计原则和思想比设计模式更加普适和重要。掌握了代码的设计原则和思想,我们能更清楚的了解,为什么要用某种设计模式,就能更恰到好处地应用设计模式。
4.总结与补充
设计意图:
Strategy模式是行为模式,正因为他是一种行为模式,所以他不是用来解决类的实例化的,跟创建什么样的产品没有关系。
Strategy模式解决的问题是把一个系列完成相同工作,却实现不同的算法(行为)包装到一系列的策略类里面,使得它们可以相互替换,提供一个访问接口,由客户端决定在什么情况下使用什么具体策略,来完成某一功能。并可以自由的添加修改相应的算法,轻松实现可插入式(Pluggable)的系统的开发。对于客户端来说,不关心实例化了那些对象,生产了那些产品,只需要提供要使用那种策略去完成某一功能。
优点:
- 策略模式提供了管理相关的算法族的办法。策略类的等级结构定义了一个算法或行为族,并能起到很好的约束作用。
- 完全支持开闭原则,算法的选择由客户端来决定,一定程度提高系统的灵活性。
- 避免程序中使用多重条件转移语句,使系统更灵活,并易于扩展。
- 替换继承关系的一种办法。
缺点:
- 每个具体策略类都会产生一个新类,所以会增加系统需要维护的类的数量。
- 客户端必须知道所有的策略类,并自行决定使用哪一个策略类(增加了客户端使用的难度)。
应用情景:
- 多个类只区别在表现行为不同,可以使用Strategy模式,在运行时动态选择具体要执行的行为。
- 需要在不同情况下使用不同的策略(算法),或者策略还可能在未来用其它方式来实现。
- 对客户隐藏具体策略(算法)的实现细节,彼此完全独立。
参考
极客时间《设计模式之美》 ----- 王争
http://www.cnblogs.com/ejiyuan/archive/2012/06/26/2564145.html
https://blog.csdn.net/lijingronghcit/article/details/107008077