设计模式学习笔记(二十四):策略模式
1 概述
1.1 引言
在外出旅游时,很多时候的出行方式都不止一条,通常根据实际情况,比如目的地,预算,旅游时间等确定最适合的出行方式。在软件开发中,也常常会遇到类似的情况,实现某一个功能有多种途径,每一条途径对应一个算法,这时可以使用一种叫做策略模式的设计模式来进行设计。在策略模式中,可以定义一些独立的类来封装不同的算法,每一个类封装一种具体的算法。
这里每一个封装的算法可以被称之为一种策略,为了保证这些策略在使用时具有一致性,一般会提供一个抽象的策略类作为规则的定义,每种具体算法对应于一个具体策略类。
策略模式的主要目的是将算法的定义与使用分开,也就是将算法的行为和环境分开,将算法的定义放在专门的策略类中,每一个策略类封装了一种实现算法,使用算法的环境类针对抽象策略类进行编程,符合DIP(依赖倒转原则)。出现新算法时只需要定义一个新的具体策略类即可。
1.2 定义
策略模式:定义一系列算法类,将每一个算法封装起来,并让他们可以相互替换。
策略模式也叫政策模式,是一种对象行为型模式。
1.3 结构图
1.4 角色
Context
(环境类):使用算法的角色,解决了某个问题时可以采用的多种策略,在环境类维持一个抽象策略类的引用实例,用于定义所采用的策略Strategy
(抽象策略类):为支持的算法声明了抽象方法,是所有策略类的父类,可以是抽象类或具体类,也可以是接口,环境类通过抽象策略类中声明的方法在运行时调用具体策略类中实现的算法ConcreteStrategy
(具体策略类):实现了抽象策略类中声明的算法,在运行时,具体策略类将覆盖在环境类中定义的抽象策略类对象,使用一种具体的算法实现某个业务处理
2 典型实现
2.1 步骤
- 定义抽象策略类:一般实现为接口,声明抽象算法
- 定义具体策略类:实现抽象策略类,实现其中的具体算法
- 定义环境类:维持一个对抽象策略类的引用,通过setter或构造函数注入具体策略类,调用时通过该抽象引用调用相应算法
2.2 抽象策略类
interface AbstarctStrategy
{
void algorithm();
}
这里定义为一个接口,只有一个抽象算法方法。
2.3 具体策略类
class ConcreteStrategy1 implements AbstarctStrategy
{
@Override
public void algorithm()
{
System.out.println("具体策略1");
}
}
class ConcreteStrategy2 implements AbstarctStrategy
{
@Override
public void algorithm()
{
System.out.println("具体策略2");
}
}
定义两个具体策略类,分别表示不同的算法。
2.4 环境类
class Context
{
private AbstarctStrategy strategy;
public void setStrategy(AbstarctStrategy strategy)
{
this.strategy = strategy;
}
public void algorithm()
{
strategy.algorithm();
}
}
通过setter注入具体策略类,在调用环境类的方法时通过抽象策略类调用其中的具体策略类的算法。
2.5 客户端
public static void main(String[] args)
{
Context context = new Context();
context.setStrategy(new ConcreteStrategy1());
context.algorithm();
context.setStrategy(new ConcreteStrategy2());
context.algorithm();
}
3 实例
设计一个电影票打折系统,有三种不同的打折方式:学生可以享受8折优惠,10周岁以下儿童可以享受减免10元优惠,VIP可以享受半价优惠,使用策略模式进行设计。
设计如下:
- 环境类:
MovieTicket
- 抽象策略类:
Discount
- 具体策略类:
StudentDiscount
+VIPDiscount
+ChildrenDiscount
首先是抽象策略类:
interface Discount
{
double calculate(double price);
}
包含一个计算折扣的方法,接着是具体策略类:
class StudenDiscount implements Discount
{
@Override
public double calculate(double price)
{
System.out.println("学生票");
return price * 0.8;
}
}
class ChildrenDiscount implements Discount
{
@Override
public double calculate(double price)
{
System.out.println("儿童票");
return price - 10.0;
}
}
class VIPDiscount implements Discount
{
@Override
public double calculate(double price)
{
System.out.println("VIP票");
return price * 0.5;
}
}
三个不同的具体策略类表示三种不同的计算折扣方式,根据需要返回对应的折扣价格。
最后是环境类:
class MovieTicket
{
private Discount discount;
private double originalPrice;
public void setPrice(double price)
{
this.originalPrice = price;
}
public void setDiscount(Discount discount)
{
this.discount = discount;
}
public double getDicountPrice()
{
return discount.calculate(originalPrice);
}
}
环境类通过setPrice
设定电影票价格后,在通过setDiscount
注入具体策略类,最后使用getDiscountPrice
获取折扣后的价格。
测试:
public static void main(String[] args)
{
MovieTicket movieTicket = new MovieTicket();
movieTicket.setPrice(100.0);
movieTicket.setDiscount(new StudenDiscount());
System.out.println(movieTicket.getDicountPrice());
movieTicket.setDiscount(new VIPDiscount());
System.out.println(movieTicket.getDicountPrice());
movieTicket.setDiscount(new ChildrenDiscount());
System.out.println(movieTicket.getDicountPrice());
}
客户端需要明确知道这三种折扣,也就是打折方式由客户端指定,输出如下:
4 JDK中的策略模式
策略模式实用性强,扩展性好,是使用频率较高的设计模式,下面来看看JDK中的典型应用。
Java SE容器布局管理器就是策略模式的一个经典应用案例,基本结构如下:
JavaSE中用户需要对容器对象Container
进行布局,在程序运行期间由客户端动态决定一个Container
对象如何布局,Java提供了几种不同的布局方式:BorderLayout
,FlowLayout
,GridLayout
,GridBagLayout
,CardLayout
。在上图结构中:
Container
充当了环境角色Context
LayoutManager
充当了抽象策略角色LayoutManager
的各个子类充当了具体策略类
Container
针对LayoutManager
进行编程,无须关心具体布局是什么,这样的设计符合里氏替换原则。
5 与状态模式的不同
状态模式与策略模式很像,下面是两者的结构图:
两者的结构图很相似,但是实际上也有很多的不同:
- 意图不同:策略模式让客户端指定更换具体策略算法,而状态模式是状态在满足一定条件自动切换,用户无法手动设置状态
- 负责范围不同:状态模式负责不同状态下对象行为的处理,而策略模式负责具体算法或策略的处理,对于算法来说都有一个明确的目标,都是在做一件事情,比如上面的电影票打折例子,无论选择何种策略,都是为了打折,但是状态模式在不同的状态下做的事情可能不同
- 封装内容不同:状态模式封装了对象的状态,而策略模式封装了具体的算法或策略
- 重用性不同:状态是跟对象密切相关的,不能重用,而策略模式的具体策略可以分离出来重用
Context
的使用不同:状态模式中每个状态持有Context
引用,实现状态切换,但是每个策略不持有Context
引用,策略只是被Context
使用- 客户端需要考虑的情况不同:对于状态模式来说,状态模式依赖于其内部状态的变化时内部的行为发生变化,状态是系统自身固有的,由系统本身控制,状态对客户端不透明,客户端不需要考虑系统的状态,也不能直接指定或改变系统的状态切换。但是对于策略模式来说,客户端需要知道所有的策略类,明确各种策略的利弊,对其进行权衡并选择策略,也就是策略需要对客户端透明,需要由客户端考虑使用何种策略
6 主要优点
- 完美支持OCP:策略模式提供了对开闭原则的完美支持,用户可以在不修改原有系统的基础上选择算法或者行为,也可以灵活提供新的算法或行为
- 易于管理和复用算法:策略模式提供了管理相关的算法族的办法,策略类的等级结构定义了一个算法或行为族,恰当使用继承可以把公共代码移到抽象策略类中以简化代码,同时由于算法单独封装在具体策略类中,可以方便复用这些算法
- 替换继承:策略模式提供了一种替换继承关系的方法,不使用策略模式的话环境类可能有子类,造成算法的使用和定义混在一起,而且使用继承的话无法实现算法或行为在运行时动态切换
- 避免多重
else if
:多重选择语句不易维护,因为将选择算法的逻辑以及算法本事实现逻辑混在一起,硬编码在一个巨大的if/else if
中,使用策略模式可以避免这种结构
7 主要缺点
- 策略类需要对客户端透明:客户端必须知道所有的策略类,并自行决定哪一个策略类,也就是客户端需要理解这些算法的区别以便选择适当的算法
- 策略类数量多:策略模式会造成系统产生很多具体策略类,任何细小的变化都会导致系统增加一个新的具体策略类
- 客户端无法使用多个策略类:客户端每次只能使用一个策略类,不支持使用一个策略类完成部分功能后再使用另一个策略类来完成剩下的功能
8 适用场景
- 一个系统需要动态在几种算法中选择一种,这些算法类均有统一的接口
- 一个对象有很多行为,使用策略模式可以将这些行为转移到相应具体策略类中
- 不希望客户端知道复杂的,与算法相关的数据结构,在具体策略类中对其进行封装