设计模式之策略模式详解
概述
先看下面的图片,我们去旅游选择出行模式有很多种,可以骑自行车、可以坐汽车、可以坐火车、可以坐飞机。
作为一个程序猿,开发需要选择一款开发工具,当然可以进行代码开发的工具有很多,可以选择Idea进行开发,也可以使用eclipse进行开发,也可以使用其他的一些开发工具。
在软件开发中,我们也常常会遇到类似的情况,实现某一个功能有多条途径,每一条途径对应一种算法,此时我们可以使用一种设计模式来实现灵活地选择解决途径,也能够方便地增加新的解决途径。
策略模式定义:
该模式定义了一系列算法,并将每个算法封装起来,使它们可以相互替换,且算法的变化不会影响使用算法的客户(算法的具体实现不同?)。策略模式属于对象行为模式,它通过对算法进行封装,把使用算法的责任和算法的实现分割开来,并委派给不同的对象对这些算法进行管理。
策略模式的主要目的是将算法的定义与使用分开,也就是将算法的行为和环境分开,将算法的定义放在专门的策略类中,每一个策略类封装了一种实现算法,使用算法的环境类针对抽象策略类进行编程,符合“依赖倒转原则”。在出现新的算法时,只需要增加一个新的实现了抽象策略类的具体策略类即可。
结构
在策略模式中,我们可以定义一些独立的类来封装不同的算法,每一个类封装一种具体的算法,在这里,每一个封装算法的类我们都可以称之为一种策略(Strategy),为了保证这些策略在使用时具有一致性,一般会提供一个抽象的策略类来做规则的定义,而每种算法则对应于一个具体策略类。
策略模式的主要角色如下:
- 抽象策略(Strategy)类:这是一个抽象角色,通常由一个接口或抽象类实现。此角色给出所有的具体策略类所需的接口,所有具体的策略类都要实现这个接口。环境(上下文)类Context 使用这个接口调用具体的策略类。
- 具体策略(Concrete Strategy)类:实现了抽象策略定义的接口,提供具体的算法实现或行为。
- 环境(Context)类:用于配置一个具体的算法策略对象 ,维持一个策略接口类型的引用( Reference ),并且可以定义一个让接口 Strategy 的具体对象访问的接口。在简单情况下,Context 类可以省略。
策略模式是一个比较容易理解和使用的设计模式,策略模式是对算法的封装,它把算法的责任和算法本身分割开,委派给不同的对象管理。策略模式通常把一个系列的算法封装到一系列具体策略类里面,作为抽象策略类的子类。在策略模式中,对环境类和抽象策略类的理解非常重要,环境类是需要使用算法的类。在一个系统中可以存在多个环境类,它们可能需要重用一些相同的算法。
在客户端代码中只需注入一个具体策略对象,可以将具体策略类类名存储在配置文件中,通过反射来动态创建具体策略对象,从而使得用户可以灵活地更换具体策略类,增加新的具体策略类也很方便。策略模式提供了一种可插入式(Pluggable)算法的实现方案。
UML类图的设计
不同的具体策略类实现了同一个抽象的策略算法接口,环境类通过聚合抽象类接口,通过多态的方式依赖具体的策略类;
用户直接依赖环境类,当具体的策略类发生变化或者有新的策略算法,用户端不需要进行修改;
案例实现
【例】促销活动
引出策略模式
一家百货公司在定年度的促销活动。针对不同的节日(春节、中秋节、圣诞节)推出不同的促销活动,由促销员将促销活动展示给客户。
该促销活动包括ABC三种不同的推销方式,最初的设计是将不同的推销策略以及用户的main方法全都封装在同一个类中;其优点是将整个程序设计为一个单独的类,比较容易写代码;缺点是没有做到责任分离,在可扩展性以及可维护性方面也存在问题。
所以最初的改进方案是将以这个单个类拆分为两个类,一个是客户端使用的main方法,一个是封装了三种推销策略的类;
很明显,这个设计将带有主方法的客户类 与具体的策略类 剥离,因此做到了责任分离。并并且因为该设计中所有的策略算法都被封装在一个类中,而这些方法都是为了完成相同的任务,即对针对节日进行产品促销, 所以该设计具有高内聚的性质。
表面上看来本设计应该说是令人满意的 其优点是当需要修改策略类中的某个方法时,不必对客户类 做任何修改。虽然不满足开闭原则,但是从可维护性上说,新的设计比原来的设计要好得多。
而且该设计仍然存在这样的问题:当一个新促销策略被添加到策略类中或当一个
算法被修改以后,整个策略类都需要重新编译,也就是说扩展性跟开闭原则都没有得到很好的满足;
策略模式改进
所以为了解决上述问题,需要进一步改进设计。进一步拆分策略类 ,将每个促销算法都单独封装在一个类中,也就是将一个类拆分成几个类,每个类都单独封装一个促销策略算法。
这样一来,修改一个算法只需重新编译算法所涉及的那个类,而不需要重新编译其他类。如
果想要添加一个新的算法 只需在子类的集合中再添加一个新的封装该算法的类即可。
类图如下:
代码如下:
定义百货公司所有促销活动的共同接口
//抽象策略类
public interface Strategy {
void show();
}
定义具体策略角色(Concrete Strategy):每个节日具体的促销活动
//为春节准备的促销活动A
public class StrategyA implements Strategy {
public void show() {
System.out.println("买一送一");
}
}
//为中秋准备的促销活动B
public class StrategyB implements Strategy {
public void show() {
System.out.println("满200元减50元");
}
}
//为圣诞准备的促销活动C
public class StrategyC implements Strategy {
public void show() {
System.out.println("满1000元加一元换购任意200元以下商品");
}
}
定义环境角色(Context):用于连接上下文,即把促销活动推销给客户,这里可以理解为销售员
public class SalesMan {
//持有抽象策略角色的引用
private Strategy strategy;
public SalesMan(Strategy strategy) {
this.strategy = strategy;
}
//向客户展示促销活动
public void salesManShow(){
strategy.show();
}
}
优缺点
1,优点:
-
策略类之间可以自由切换
由于策略类都实现同一个接口,所以使它们之间可以自由切换。
-
易于扩展
增加一个新的策略只需要添加一个具体的策略类即可,基本不需要改变原有的代码,符合“开闭原则“
-
避免使用多重条件选择语句(if else),充分体现面向对象设计思想。
2,缺点:
- 客户端必须知道所有的策略类,并自行决定使用哪一个策略类。也就是说客户程序必须知道不同策略接口的各个子类的行为,必须理解每个子类有哪些不同。
- 策略模式将造成产生很多策略类,可以通过使用享元模式在一定程度上减少对象的数量。
- 无法同时在客户端使用多个策略类,也就是说,在使用策略模式时,客户端每次只能使用一个策略类,不支持使用一个策略类完成部分功能后再使用另一个策略类来完成剩余功能的情况。
使用场景
- 一个系统需要动态地在几种算法中选择一种时,当有多个仅在行为上不同但是任务相关的类存在时,可将每个算法封装到策略类中。
- 一个类定义了多种行为,并且这些行为在这个类的操作中以多个条件语句的形式出现,可将每个条件分支移入它们各自的策略类中以代替这些条件语句。
- 系统中各算法彼此完全独立,且要求对客户隐藏具体算法的实现细节时。
- 当一个算法使用的用户不应该知道的数据时 使用策略模式可以将算法实现细节隐藏起来 ,避免暴露与算法相关的复杂细节。注意 虽然可以将算法实现细节封装起来,但是客户程序必须知道各个策略子类的接口。
关于策略模式的讨论
使用策略模式主要有两个出发点:
(1 ) 将一组相关的算法封装为各个策略分支,从而将策略分支相关的代码隐藏起来。
(2) 希望可以提升程序的可扩展性。
下面我们就策略模式的可扩展性进行简单的讨论,实际上 策略模式的初衷是要减少与各个分支下的行为相关的条件语句。这已经通过将一个具有条件相关的多种行为的类拆分成一个策略超类与若干个策略子类得到了解决。也就是说,将原来的一个单独的但是包含多个条件语句的类改变为一个没有条件语句的策略层次类。
这里虽然看似条件语句消失了,但是在客户程序与 Context 类中是否也不存在与策略子类相关的条件语句了呢?答案当然不是。
实际上一般在策略模式的设计中 客户类根据不同的条件负责创建不同的策略子类的对象,然后再将该对象传递给 Context 环境类,Context 类的作用可以理解为:为被调用策略子类的一些方法提供一些参数,以及使用该由 Client 类传入的对象去调用 Strategy 类的某些方法。
这说明 在客户类 Client 中 存在许多与策略分支子类相关的条件语句,而在 Context 类中,没有这样的语句。
那么 是否可以将创建策略子类的对象的责任交给 Context 类,而客户类 Client 只为 Context 类提供一些代表客户请求的参数呢?
(1 ) 客户类负责创建策略子类的对象的情况
**客户类根据用户提供的不同的请求,负责创建不同的策略子类的对象 ,然后再将该对象传递
给 Context 类。**在这种情况下,客户类中通常包含与策略相关的条件语句,而在 Context 类中不必
使用任何与策略有关的条件语句,因此,修改或者添加一个策略子类都不必修改 Context 类。但
是,在添加一个新的策略子类的情况下,如果客户类需要使用该子类,往往需要在客户类中添
加一个新的条件语句,即客户类需要修改。
(2) Context 类负责创建策略子类的对象的情况
将创建策略子类的对象的责任交给 Context 类, 而客户类 Client 只为 Context 类提供一些代
表客户请求的参数 ;在此情况下,Context 类在创建策略子类的对象时,必然会使用与策略子
类有关的条件语句。此时,修改一个策略子类不需要修改客户类与 Context 类。而在添加一个
新的策略子类时,如果此时客户类暂时不使用该新的子类,则新子类的添加不会影响客户类
与 Context 类的源代码。但是,如果客户类要使用新的策略子类,则必须同时在客户类与 Con- text 类中添加新的条件分支,也就是说,需要同时修改客户类与 Context 类。
在以上两种情况下,当只是需要修改策略子类的代码时,客户类与 Context 类都不需要进行修改。
综上所述 由客户类创建对象的设计可扩展性好一些。这样,可以做到在 Context 类中出现与策略子类相关的条件语句,从而可扩展性也得到了提高。