目录
本文的结构如下:
- 引言
- 什么是策略模式
- 模式的结构
- 典型代码
- 代码示例
- 策略模式和模板方法模式的区别
- 优点和缺点
- 适用环境
- 模式应用
一、引言
写这篇文章的时间是17年11月18号上午9点半,NBA正打得火热,骑士VS76人,詹韦对阵大帝和西帝,内线孱弱的骑士被大帝蹂躏,欲生欲死;再看火箭VS篮网,第一节哈登7中7,三分5中5,狂砍20分;又有步行者VS魔术,奥拉迪波10中10......可谓精彩。
我们的话题由此展开。
篮球而言,对阵双方4节比赛结束后,哪队分数高,哪队获胜,打平加时继续较量。所以,说到最后,篮球是一个得分的比赛,而得分的方式又有许多种,库日天的三分,老詹的上篮,德罗赞的中投,哈登的罚球(我不是黑,不要喷我,只是选择比较有代表性的得分方式)。具体的得分方式根据防守而定,篮下空无一人果断暴扣,三分线外没人防守,三分扔起,当然老汉日常碧浪就不说了......
在软件开发中,也有很多类似的场景,实现某一个功能有多条途径,每一条途径对应一种算法,此时可以使用一种设计模式来实现灵活地选择解决途径,也能够方便地增加新的解决途径。这就是策略模式。
二、什么是策略模式
在策略模式中,定义了一些独立的类来封装不同的算法,每一个类封装一种具体的算法,在这里,每一个封装算法的类都可以称之为一种策略(Strategy),为了保证这些策略在使用时具有一致性,一般会提供一个抽象的策略类来做规则的定义,而每种算法则对应于一个具体策略类。
策略模式的主要目的是将算法的定义与使用分开,也就是将算法的行为和环境分开,将算法的定义放在专门的策略类中,每一个策略类封装了一种实现算法,使用算法的环境类针对抽象策略类进行编程,符合“依赖倒转原则”。在出现新的算法时,只需要增加一个新的实现了抽象策略类的具体策略类即可。
策略模式定义如下:
策略模式(Strategy Pattern):定义一系列算法类,将每一个算法封装起来,并让它们可以相互替换,策略模式让算法独立于使用它的客户而变化,也称为政策模式(Policy)。策略模式是一种对象行为型模式。
三、模式的结构
策略模式的UML类图如下:
在策略模式结构图中包含如下几个角色:
- Context(环境类):环境类是使用算法的角色,它在解决某个问题(即实现某个方法)时可以采用多种策略。在环境类中维持一个对抽象策略类的引用实例,用于定义所采用的策略。
- Strategy(抽象策略类):它为所支持的算法声明了抽象方法,是所有策略类的父类,它可以是抽象类或具体类,也可以是接口。环境类通过抽象策略类中声明的方法在运行时调用具体策略类中实现的算法。
- ConcreteStrategy(具体策略类):它实现了在抽象策略类中声明的算法,在运行时,具体策略类将覆盖在环境类中定义的抽象策略类对象,使用一种具体的算法实现某个业务处理。
四、典型代码
策略模式是一个比较容易理解和使用的设计模式,策略模式是对算法的封装,它把算法的责任和算法本身分割开,委派给不同的对象管理。策略模式通常把一个系列的算法封装到一系列具体策略类里面,作为抽象策略类的子类。环境类是需要使用算法的类。在一个系统中可以存在多个环境类,它们可能需要重用一些相同的算法。
在使用策略模式时,首先应该创建一个抽象策略类,其典型代码如下所示:
public abstract class AbstractStrategy {
public abstract void algorithm();
}
封装每一种具体算法的类作为该抽象策略类的子类,期典型代码如下:
public class ConcreteStrategyA extends AbstractStrategy {
public void algorithm() {
//todo
}
}
public class ConcreteStrategyB extends AbstractStrategy {
public void algorithm() {
//todo
}
}
在Context类与抽象策略类之间建立一个关联关系,其典型代码如下:
public class Context {
private AbstractStrategy strategy;
public Context(AbstractStrategy strategy){
this.strategy = strategy;
}
public void algorithm(){
strategy.algorithm();
}
}
客户端代码如下:
public class Client {
public static void main(String[] args) {
AbstractStrategy strategy = new ConcreteStrategyA();
Context context = new Context(strategy);
context.algorithm();
}
}
也可以将具体策略类类名存储在配置文件中,通过反射来动态创建具体策略对象,从而使得用户可以灵活地更换具体策略类,增加新的具体策略类也很方便。策略模式提供了一种可插入式(Pluggable)算法的实现方案。
五、代码示例
这里以一个球员上场打球为例说明。
5.1、不用策略模式
设计一个球员类:
public class Player {
private String defensive;//对手的防守情况
public Player(){
}
public void setDefensive(String defensive){
this.defensive = defensive;
}
public void score(){
if ("undefended".equalsIgnoreCase(defensive)){
System.out.println("篮下无人防守,他起飞,大风车战斧扣篮,但是扣飞了,得0分,what a pity!");
}else if("foul".equalsIgnoreCase(defensive)){
System.out.println("他身上挂着三个人,强起上篮命中,哨响了,and one,他走上罚球线,加罚不中,得两分");
}else if("1s".equalsIgnoreCase(defensive)){
System.out.println("比赛剩下最后1s,还落后2分,现在比赛回来,他接到了球,没时间了,后撤步直接射三分,有没有?命中了,绝杀,绝杀。");
}
}
}
客户端测试:
public class Client {
public static void main(String[] args) {
Player player = new Player();
String defensive;//对手防守情况
player.setDefensive("undefended");
player.score();
System.out.println("------------------------");
player.setDefensive("foul");
player.score();
System.out.println("------------------------");
player.setDefensive("1s");
player.score();
System.out.println("------------------------");
}
}
这段代码实现了根据不同防守得分的方式,防守方式不同,直接修改客户端的参数,而不需修改源码,但还是有一些问题:
- 它包含各种得分的方式,代码很多且复杂,if...else...多,不利于维护和扩展。
- 增加新的得分方式必须修改源码,违反了“开闭原则”,系统的灵活性和可扩展性较差。
- 复用性差,如果另一个球员想要复用这些得分方式,只能将源码粘贴复制,无法单独重用其中的某个或某些算法(重用较为麻烦)。
5.1、使用策略模式
重构后Basket充当抽象策略类,FreeThrows、SlamDunk和ThreePointer充当具体策略类,Player充当环境类。
代码如下:
策略类:
public abstract class Basket {
public abstract void score();
}
public class SlamDunk extends Basket{
public void score() {
System.out.println("篮下无人防守,他起飞,大风车战斧扣篮,但是扣飞了,得0分,what a pity!");
}
}
public class FreeThrows extends Basket{
public void score() {
System.out.println("他身上挂着三个人,强起上篮命中,哨响了,and one,他走上罚球线,加罚不中,得两分");
}
}
public class ThreePointer extends Basket {
public void score() {
System.out.println("比赛剩下最后1s,还落后2分,现在比赛回来,他接到了球,没时间了,后撤步直接射三分,有没有?命中了,绝杀,绝杀。");
}
}
环境类:
public class Player {
private Basket basket;
public Player(Basket basket){
this.basket = basket;
}
public void score(){
basket.score();
}
}
客户端:
public class Client {
public static void main(String[] args) {
Basket basket = new ThreePointer();
Player player = new Player(basket);
player.score();
}
}
重构后的代码,如果需要增加新的得分方式,原有代码均无须修改,只要增加一个新的得分类作为抽象得分类的子类,实现在抽象得分类中声明的得分方法,然后可以通过配置文件的方式更换具体子类,完全符合“开闭原则”。
六、策略模式和模板方法模式的区别
Template Method模式是将相同的算法放在一个类中,将算法变化的部分放在子类中实现,策略模式是将不同的算法用不同的策略类表示,似乎没有太大的区别。
认真想想还是有区别的。
策略模式的策略类中的方法一般是public的,封装的算法是任意给用户使用,而模板方法模式中的虚方法更多的是有限制的,一般不希望被外部调用,而是在模板方法中在固定的顺序位置被调用。
七、优点和缺点
7.1、优点
策略模式的主要优点如下:
- 策略模式提供了对“开闭原则”的完美支持,用户可以在不修改原有系统的基础上选择算法或行为,也可以灵活地增加新的算法或行为。
- 策略模式提供了管理相关的算法族的办法。策略类的等级结构定义了一个算法或行为族,恰当使用继承可以把公共的代码移到抽象策略类中,从而避免重复的代码。
- 策略模式提供了一种可以替换继承关系的办法。如果不使用策略模式,那么使用算法的环境类就可能会有一些子类,每一个子类提供一种不同的算法。但是,这样一来算法的使用就和算法本身混在一起,不符合“单一职责原则”,决定使用哪一种算法的逻辑和该算法本身混合在一起,从而不可能再独立演化;而且使用继承无法实现算法或行为在程序运行时的动态切换。
- 使用策略模式可以避免多重条件选择语句。多重条件选择语句不易维护,它把采取哪一种算法或行为的逻辑与算法或行为本身的实现逻辑混合在一起,将它们全部硬编码(Hard Coding)在一个庞大的多重条件选择语句中,比直接继承环境类的办法还要原始和落后。
- 策略模式提供了一种算法的复用机制,由于将算法单独提取出来封装在策略类中,因此不同的环境类可以方便地复用这些策略类。
7.2、缺点
策略模式的主要缺点如下:
- 客户端必须知道所有的策略类,并自行决定使用哪一个策略类。这就意味着客户端必须理解这些算法的区别,以便适时选择恰当的算法。换言之,策略模式只适用于客户端知道所有的算法或行为的情况。
- 策略模式将造成系统产生很多具体策略类,任何细小的变化都将导致系统要增加一个新的具体策略类。
- 无法同时在客户端使用多个策略类,也就是说,在使用策略模式时,客户端每次只能使用一个策略类,不支持使用一个策略类完成部分功能后再使用另一个策略类来完成剩余功能的情况。
八、适用环境
在以下情况下可以考虑使用策略模式:
- 如果在一个系统里面有许多类,它们之间的区别仅在于它们的行为,那么使用策略模式可以动态地让一个对象在许多行为中选择一种行为。
- 一个系统需要动态地在几种算法中选择一种,那么可以将这些算法封装到一个个的具体算法类中,而这些具体算法类都是一个抽象算法类的子类。
- 一个对象有很多的行为,如果不用恰当的模式,这些行为就只好使用多重条件选择语句来实现。此时,使用策略模式,把这些行为转移到相应的具体策略类里面,就可以避免使用难以维护的多重条件选择语句。
- 不希望客户端知道复杂的、与算法相关的数据结构,在具体策略类中封装算法与相关的数据结构,可以提高算法的保密性与安全性。
九、模式应用
Java SE的容器布局管理就是策略模式的一个经典应用实例,其基本结构如下图: