9 策略模式 Strategy Pattern
9.1 概述
策略模式是为了解决软件开发中存在的多种算法灵活切换的问题,如果使用硬编码(Hard Coding)会导致系统违背开闭原则,扩展性差,且维护困难,这个时候我们可以通过策略模式来定义类封装不同的算法。
定义:定义一系列算法,将每一个算法封装起来,并让它们可以相互替换。策略模式让算法可以独立于使用它的客户变化。
对象行为型模式
- 又称为政策(Policy)模式
- 每一个封装算法的类称之为策略(Strategy)类
- 策略模式提供了一种可插入式(Pluggable)算法的实现方案
9.2 结构与实现
9.2.1 结构
策略模式包含以下3中角色:
Context
(环境类)Strategy
(抽象策略类)ConcreteStrategy
(具体策略类)
9.2.2 实现
你会发现结构图其实和状态模式相比只是对应的类换了一个名字,这其实是一种面向对象思想在不同使用场景下的具体演化,我们都会通过抽象来将多个类的共同特征抽象为一个抽象类,并通过聚合来将该抽象类与相关对象以成员变量的形式聚合(or 组合)在一起,这样方便后续代码的扩展,如果新增了具体类,只需要实现抽象类的接口就可以完成系统的扩展,而不需要修改原先的代码。
典型的抽象策略类代码(也可以声明为接口)
public abstract class Strategy { |
典型的具体策略类代码
public class ConcreteStrategy extends Strategy { |
典型的环境类代码
public class Context { |
典型的客户端代码片段。
Context context = new Context(); |
9.3 实例
9.4 AWT + Swing中的布局管理
Java GUI编程中的 LayoutManager
其实就用到了策略模式来实现对不同布局的应用,通过将不同布局封装为不同的 策略类,用户在使用时只需要通过 setter
注入想要使用的布局就可以获得想要的效果。
9.5 优缺点与适用环境
9.5.1 优点
- 提供了对开闭原则的完美支持,用户可以在不修改原有系统的基础上选择算法或行为,也可以灵活地增加新的算法或行为
- 提供了管理相关的算法族的办法
- 提供了一种可以替换继承关系的办法
- 可以避免多重条件选择语句
- 提供了一种算法的复用机制,不同的环境类可以方便地复用策略类
9.5.2 缺点
- 客户端必须知道所有的策略类,并自行决定使用哪一个策略类
- 将造成系统产生很多具体策略类
- 无法同时在客户端使用多个策略类
9.5.3 适用环境
- 一个系统需要动态地在几种算法中选择一种
- 避免使用难以维护的多重条件选择语句
- 不希望客户端知道复杂的、与算法相关的数据结构,提高算法的保密性与安全性
10 模版方法模式 Template Method Pattern
10.1 概述
定义:模版方法模式定义了一个操作中的算法的框架,而将一些步骤延迟到子类中。模版方法模式使得子类不改变一个算法的结构即可重定义该算法的某些特定步骤的实现
类行为型模式
- 是一种基于继承的代码复用技术
- 将一些复杂流程的实现步骤封装在一系列基本方法中
- 在抽象父类中提供一个称之为模版方法的方法来定义这些基本方法的执行次序,而通过其子嘞来覆盖某些步骤,从而使得相同的算法框架可以有不同的执行结果
10.2 结构与实现
10.2.1 结构
模版方法模式包含以下两个角色:
AbstractClass
(抽象类)ConcreteClass
(具体子类)
10.2.2 实现
模版方法模式的实现主要为分两个部分:
- 模版方法(Template Method)
- 基本方法(Primitive Method)
- 抽象方法(Abstract Method)
- 具体方法(Concrete Method)
- 钩子方法(Hook Method)
典型的模版方法:
/** |
典型的抽象类实现:
public abstract class AbstractClass { |
子类典型代码
public class ConcreteClass extends AbstractClass { |
对于客户端来说,其实只需要调用模版方法即可,因此具体流程的方法其实可以实现为
protected
(子类不需要知道的应该实现为private
)。在C++
中,这是一种非常常见的实现一些无法实现为虚函数的“虚化”过程
例:对于友元函数,我们是没办法声明为虚函数的,但是某些友元函数又需要“虚化”的性质以便子类能够在父类基础上进行重写,比如对于 cout
,我们不希望子类再去写一遍重载 <<
的友元函数实现。
|
10.3 优缺点与适用环境
10.3.1 优点
- 在父类中形式化地定义一个算法,而由它的子类来实现细节的处理,在子类实现详细的处理算法时并不会改变算法中步骤的执行次序
- 提取了类库中的公共方法,将公共行为放在父类中,而通过其子类来实现不同的行为
- 可实现一种反向控制结构,通过子类覆盖父类的钩子方法来决定某一特定步骤是否需要执行
- 更换和增加新的子类很方便,符合单一职责原则和开闭原则
10.3.2 缺点
需要为每一个基本方法的不同实现提供一个子类,如果父类中可变的基本方法太多,将会导致类的个数增加,系统会更加庞大,设计也更加抽象(可结合桥接模式)
10.3.3 适用环境
- 一次性实现一个算法的不变部分,并将可变的行为留给子类来实现
- 各子类中公共的行为应该被提取出来,并集中到一个公共父类中,以避免代码重复
- 需要通过子类来决定父类算法中某个步骤是否执行,实现子类对父类的反向控制
11 访问者模式 Visitor Pattern
11.1 概述
- 对象结构中存储了多种不同类型的对象信息
- 对同一对象结构中的元素的操作方式并不唯一,可能需要提供多种不同的处理方式
- 还有可能需要增加新的处理方式
定义:访问者模式标识一个作用于某对象结构中的各个元素的操作。访问者模式让你可以在不改变各元素的类的前提下定义作用于这些元素的新操作
对象行为型模式
- 访问者模式为操作存储不同类型元素的对象结构提供了一种解决方案
- 用户可以对不同类型的元素施加不同的操作
11.2 结构与实现
11.2.1 结构
访问者模式包含以下5个角色
Visitor
(抽象访问者)ConcreteVistor
(具体访问者)Element
(抽象元素)ConcreteElement
(具体元素)ObjectStructure
(对象结构)
11.2.2 实现
典型的抽象访问者类代码
public abstract class Visitor { |
典型的具体访问者类代码
public class ConcreteVisitor extends Visitor { |
典型的抽象元素类代码
public interface Element { |
典型的具体元素类代码
public class ConcreteElementA implements Element { |
双重分派机制
- 调用具体元素的
accept(Visitor visitor)
方法,并将Visitor
的具体对象作为其参数 - 在具体元素类的
accept(Visitor visitor)
方法内部调用传入的Visitor
所实现的visit()
方法,比如visit(ConcreteElementA elementA)
将当前具体元素的对象(this)作为参数,例如visitor.visit(this)
- 执行
Visitor
对象的visit()
方法,在其中 还可以调用具体元素对象的业务方法。
两次分派:
- 第一次是具体元素类调用
accept()
方法时,根据不同的Visitor
对象分派不同的visitor()
方法。- 第二次是
Visitor
调用visit()
方法时,可以根据不同的具体元素类来调用不同的业务方法
典型对象结构类的实现
import java.util.*; |
11.3 优缺点与适用环境
11.3.1 优点
- 增加新的访问操作很方便
- 将有关元素对象的访问行为集中到一个访问者对下个闹钟功能,而不是分散在一个个的元素类中,类的职责更加清晰
- 让用户能够在不修改现有元素类层次结构的情况下,定义作用于该层次结构的操作
11.3.2 缺点
- 增加新的元素类很困难
- 破坏了对象的封装性
11.3.3 适用环境
- 一个对象结构包含多个类型的对象,希望这些对象实施一些依赖其具体类型的操作
- 需要对一个对象结构中的对象进行很多不同的且不相关的操作,并需要避免让这些操作“污染”这些对象的类,也不希望在增加新操作时修改这些类
- 对象结构中对象对应的类很少改变,但经常需要在此对象结构上定义新的操作
Reference
- Java设计模式 – 刘伟,清华大学出版社
- 面向对象设计方法 – 南京大学计算机科学与技术系2022春季课程
- Design Pattern – GoF 23种设计模式