文章目录
行为模式简介
行为模式用于描述
程序在运行时复杂的流程控制
,即描述多个类或对象之间怎样相互协作共同完成单个对象都无法独立完成的任务,涉及算法与对象间职责的分配。
行为模式分为
类行为模式
和对象行为模式
,前者采用继承机制来在类间分派行为,后者采用组合或聚合在对象间分配行为。由于组合关系或聚合关系比继承关系耦合度更低,满足"合成复用原则",所以对象行为模式比类行为模式具有更大的灵活性。
行为模式是GoF中最为庞大的一类,它包含
模版方法模式
、策略模式
、命令模式
、职责链模式
、状态模式
、观察者模式
、中介者模式
、迭代器模式
、访问者模式
、备忘录模式
、解释器模式
具体使用见下文。
模版方法模式(定义算法骨架和流程,子类实现可变部分
)
定义
模版方法模式:定义一个操作中的算法骨架,而将算法的一些步骤延迟到子类中,使得子类可以不改变该算法结构的前提下重定义该算法的某些特定的步骤。它是一种类行为模式。
特点
优点
- 封装不变部分,扩展可变部分。它把认为是不变部分的算法封装到父类中实现,而把可变部分算法由子类继承实现,便于子类继续扩展。
- 它在父类中提取了公共的代码部分,便于代码的复用。
- 部分方法由子类实现,因此子类可以通过扩展方式增加相应的功能,符合
开闭原则
。
缺点
- 对每个不同的实现都需要定义一个子类,会导致类的个数增加,系统更加庞大,设计也更加抽象。
- 父类中的抽象方法由子类实现,子类执行的结果会影响到父类的结果,这导致一种反向的控制结构,它提高了代码阅读的难度。
结构
模版方法模式主要包含以下主要角色
-
抽象类(Abstract Class):负责定义算法的轮廓和骨架。它由一个模版方法和若干个基本方法构成,这些方法定义如下:
模版方法:定义了算法的骨架,按某种顺序调用其包含的基本方法。
- 基本方法:整个算法中的一个步骤,包含以下几种:
- 抽象方法:在抽象类中声明,由具体子类实现。
- 具体方法:在抽象类中已经实现,在具体子类中可以继承或重写它。
- 钩子方法:在抽象类中已经实现,包括用于判断的逻辑方法和需要子类重写的空方法两种。
- 基本方法:整个算法中的一个步骤,包含以下几种:
-
具体子类(Concreate Class):实现抽象类中所定义的抽象方法和钩子方法,它们是一个顶级逻辑的一个组成步骤。
结构如下:
实现
例如:我们一家人一天中包含:起床、吃饭、做事情、看电视、睡觉几个流程,其中起床、吃饭、看电视、睡觉对每个人都是一样的,但做事情因人不同而不同(爸爸上班、妈妈收拾家务、孩子上学),因此我们采用模版方法模式来实现
-
先定义一个抽象类,包含了一个模版方法,该方法包含了起床、吃饭、做事情、睡觉几个基本方法,其中有些方法的处理由于各人都一样,所以在该抽象类中实现就可以了。
/** *定义抽象类-家庭日常(包含一个模版方法+若干个基本方法) */ public abstract class AbsFamilyDaily { /** * 模版方法(内部按照顺序调用基本方法) */ final public void templateMethod() { getUp(); eatBreakfast(); doSomething(); watchingTV(); sleep(); } //具体方法-起床 protected void getUp() { System.out.println("07:00 起床 "); } //具体方法-吃饭 protected void eatBreakfast() { System.out.println("08:00 开始吃早餐 "); } //抽象方法-做事情,每个人不一样由具体子类实现 protected abstract void doSomething(); //具体方法-看电视 protected void watchingTV(){ System.out.println("20:00 看电视 "); } //具体方法-睡觉 protected void sleep() { System.out.println("21:00 睡觉 "); } }
-
做事情每个人是不同的因此必须在具体子类中实现
/** *具体子类-爸爸,继承抽象类,实现做事方法 */ public class Father extends AbsFamilyDaily { @Override public void doSomething() { System.out.println("09:00 爸爸去上班了..."); } }
/** *具体子类-妈妈,继承抽象类,实现做事方法 */ public class Mather extends AbsFamilyDaily { @Override public void doSomething() { System.out.println("09:30 妈妈开始做家务..."); } }
/** *具体子类-孩子,继承抽象类,实现做事方法 */ public class Children extends AbsFamilyDaily { @Override public void doSomething() { System.out.println("09:10 宝宝去上学了..."); } }
-
测试
AbsFamilyDaily father = new Father(); father.templateMethod(); System.out.println("---------------"); AbsFamilyDaily mather = new Mather(); mather.templateMethod(); System.out.println("---------------"); AbsFamilyDaily children = new Children(); children.templateMethod(); //输出结果 07:00 起床 08:00 开始吃早餐 09:00 爸爸去上班了... 20:00 看电视 21:00 睡觉 --------------- 07:00 起床 08:00 开始吃早餐 09:30 妈妈开始做家务... 20:00 看电视 21:00 睡觉 --------------- 07:00 起床 08:00 开始吃早餐 09:10 宝宝去上学了... 20:00 看电视 21:00 睡觉
应用场景
模版方法模式,适用于以下场景
- 算法整体步骤很固定,但其中个别部分易变,这时候可以使用模版方法模式,将容易变动的部分抽象出来,供具体子类实现。
- 多个子类存在公共行为时,可以将其提取出来并集中到一个公共父类中以避免代码重复。首先,要识别现有代码中的不同之处,并且将不同之处分离为新的操作。最后,用一个调用这些新的操作的模版方法来替换这些不同的代码。
- 当需要控制子类的扩展时,模版方法只在特定点调用钩子操作,这样就只允许在这些点进行扩展。
扩展
模版方法模式中,基本方法包含:抽象方法
、具体方法
、钩子方法
,正确使用钩子方法
可以使得子类控制父类的行为。如下例子中,可以通过具体子类中重写钩子方法hookMethod()
来改变抽象父类中的运行结果,结构如下:
同样上面的案例,宝宝不想看电视,我们只需要在抽象类中增加一个抽象钩子方法即可,具体子类实现钩子方法来控制父类中的运行结果。
/**
*抽象类-家庭日常(增加钩子方法)
*/
public abstract class AbsFamilyDaily {
/**
* 模版方法(内部按照顺序调用基本方法)
*/
final public void templateMethod() {
getUp();
eatBreakfast();
doSomething();
if(hookMethod()){
watchingTV();
}
sleep();
}
//具体方法-起床
protected void getUp() {
System.out.println("07:00 起床 ");
}
//具体方法-吃饭
protected void eatBreakfast() {
System.out.println("08:00 开始吃早餐 ");
}
//抽象方法-做事情,每个人不一样由具体子类实现
protected abstract void doSomething();
//具体方法-看电视
protected void watchingTV(){
System.out.println("20:00 看电视 ");
}
//具体方法-睡觉
protected void sleep() {
System.out.println("21:00 睡觉 ");
}
//抽象方法-钩子,通过具体子类实现,改变父类执行逻辑
protected abstract boolean hookMethod();
}
/**
*具体子类-孩子实现抽象钩子方法
*/
public class Children extends AbsFamilyDaily {
@Override
public void doSomething() {
System.out.println("09:10 宝宝去上学了...");
}
//宝宝实现具体的钩子方法-返回false不想看电视
@Override
protected boolean hookMethod() {
return false;
}
}
//同样爸爸妈妈如看电视其具体子类中也实现该钩子方法,返回true即可
//输出结果:
07:00 起床
08:00 开始吃早餐
09:00 爸爸去上班了...
20:00 看电视
21:00 睡觉
---------------
07:00 起床
08:00 开始吃早餐
09:30 妈妈开始做家务...
20:00 看电视
21:00 睡觉
---------------
07:00 起床
08:00 开始吃早餐
09:10 宝宝去上学了...
21:00 睡觉
策略模式(定义一系列算法族,每个封装起来,使其可以相互替换
)
定义
策略模式:定义了一系列算法,并将每个算法封装起来,使它们可以相互替换,且算法的变化并不会影响使用算法的客户。策略模式属于
对象行为模式
,它通过对算法进行封装,把使用算法的责任和算法的实现分割开来,并委派给不同的对象对这些算法进行管理。
但我感觉在基本的策略模式中,选择所用具体实现的职责由客户端对象承担,并转给策略模式的Context对象[DPE]。这本身并没有解除客户端需要选择判断的压力,而策略模式与简单工厂模式结合后,选择具体实现的职责也可以由Context来承担,这就最大化地减轻了客户端的职责。
特点
优点
- 避免使用多重条件语句,多重条件语句不利于维护。
- 提供一系列可供重用的算法族,恰当使用继承可以把算法族的公共代码转移到父类里面,从而避免重复代码。
- 可提供相同行为不同实现,客户可依据不同时间或空间选择。
- 完美支持开闭原则,可以在不修改源代码情况下,灵活增加新算法。
- 把算法使用放到环境类中,而算法的实现放到具体策略类中,实现二者分离。
缺点
- 客户端必须理解所有策略算法的区别,以便适时选择恰当的算法类。
- 造成很多的策略类。
结构
策略模式是准备一组算法,并将这些算法封装到一系列具体策略类里面,作为一个抽象策略类的子类。策略模式的重心不是如何实现算法,而是如何组织这些算法,从而让程序结构更加灵活,具有更好维护性和扩展性,基本结构如下:
策略模式主要角色如下:
- 抽象策略(Abstract Strategy)类:定义一个公共接口,各种不同的算法以不同的方式实现这个接口,环境角色使用这个接口调用不同的算法,一般使用接口或抽象类实现。
- 具体策略(Concrete Strategy)类:实现了抽象策略定义的接口,提供具体的算法实现。
- 环境(Context)类:持有一个策略类引用,最终给客户端调用
结构如下:
实现
例如,我们通常在开发过程中,为了安全性我们通常采用加密算法对我们的敏感字段进行加密处理,这里加密的方式有很多中(RSA、3DES、AES)等等,因此我们通过使用加密算法来讲解策略模式使用
-
定义一个抽象策略类,声明公共接口加密
public interface AbsEncrypt { //声明公共接口-加密 void encrypt(); }
-
声明具体策略类实现抽象接口中的方法
//具体策略类-AES加密 public class AESEncrypt implements AbsEncrypt { @Override public void encrypt() { System.out.println("您当前选择的AES对称加密算法"); } }
//具体策略类-DESede加密 public class DESedeEncrypt implements AbsEncrypt{ @Override public void encrypt() { System.out.println("您当前选择的3DES对称加密算法"); } }
//具体策略类-RSA加密 public class RSAEncrypt implements AbsEncrypt { @Override public void encrypt() { System.out.println("您当前选择的RSA非对称加密算法"); } }
-
定义环境类(持有一个策略类的引用,供客户端调用)
public class Context { //持有一个策略类的引用,供客户端调用 AbsEncrypt encrypt; public AbsEncrypt getEncrypt() {return encrypt;} public void setEncrypt(AbsEncrypt encrypt) {this.encrypt = encrypt;} public void encrypt(){encrypt.encrypt();} }
-
测试:客户端调用
Context context = new Context(); AbsEncrypt aesEncrypt = new AESEncrypt(); context.setEncrypt(aesEncrypt); context.encrypt(); AbsEncrypt desedeEncrypt = new DESedeEncrypt(); context.setEncrypt(desedeEncrypt); context.encrypt(); AbsEncrypt rsaEncrypt = new RSAEncrypt(); context.setEncrypt(rsaEncrypt); context.encrypt(); //输出结果 您当前选择的AES对称加密算法 您当前选择的3DES对称加密算法 您当前选择的RSA非对称加密算法
应用场景
- 一个系统需要动态的在几种算法中选择一种时,可将每个算法封装到具体策略类中。
- 一个类定义了多种行为,并且这些行为在这个类操作中以多个条件语句出现,可将每个条件分支移植到它们对应的具体策略类中以代替这些条件语句。
- 系统中各算法彼此完全独立,且要求对客户隐藏具体到算法实现细节。
- 多个类只区别在表现行为不同,可以使用策略模式,在运行时动态的选择要执行的行为。
扩展
1. 策略工厂模式(策略模式+简单工厂模式)
在一个使用策略模式的系统中,当存在的策略很多时,客户端管理所有策略算法将变得很复杂,如果在环境类中使用
策略工厂模式
来管理这些策略类将大大减少客户端的工作复杂度,其结构图如下 所示。
代码案例:
还是如上案例,加密算法使用策略工厂模式
实现。
//抽象策略类
public interface AbsEncrypt {
//声明公共接口-加密
void encrypt();
}
//具体策略类-AES加密
public class AESEncrypt implements AbsEncrypt {
@Override
public void encrypt() {
System.out.println("您当前选择的AES对称加密算法");
}
}
//具体策略类-RSA加密
public class RSAEncrypt implements AbsEncrypt {
@Override
public void encrypt() {
System.out.println("您当前选择的RSA非对称加密算法");
}
}
//具体策略类-DESede加密
public class DESedeEncrypt implements AbsEncrypt{
@Override
public void encrypt() {
System.out.println("您当前选择的3DES对称加密算法");
}
}
//策略工厂
public class StrategyFactory {
HashMap<String,AbsEncrypt> map = new HashMap<>();
public void put(String key,AbsEncrypt absEncrypt){
map.put(key,absEncrypt);
}
protected AbsEncrypt get(String key){
return map.get(key);
}
public void encrypt(String key){
get(key).encrypt();
}
}
测试:输出
//扩展,策略工厂模式
StrategyFactory strategyFactory = new StrategyFactory();
strategyFactory.put("RSA",new RSAEncrypt());
strategyFactory.put("AES",new AESEncrypt());
strategyFactory.put("DESede",new DESedeEncrypt());
strategyFactory.encrypt("AES");
//输出
您当前选择的AES对称加密算法
命令模式(将系统中的相关操作抽象成命令,使调用者与实现者相关分离
)
定义
将一个请求封装位一个对象,使发出请求的责任和执行请求的责任分隔开。这样两者之间通过命令对象进行沟通,这样方便命令对象进行存储、传递、调用、增加和管理。
特点
优点
- 降低系统的耦合度。命令模式能将调用操作的对象与实现该操作的对象解耦。
- 增加或删除命令非常方便。采用命令模式增加与删除命令不会影响其他类,满足
开闭原则
,对扩展比较灵活。 - 可实现宏命令。命令模式可以与
组合命令
结合,将多个命令装配成一个组合命令,即宏命令。 - 方便实现Undo和Redo操作。命令模式可以与后面介绍的
备忘录模式
结合,实现命令的撤销与恢复。
缺点
- 增加系统的复杂度,可能产生大量具体命令类,因此针对每一个具体的操作都要设计一个具体命令类。
结构
命令模式包含以下主要角色
-
抽象命令类(Abstract Command)角色:声明执行命令的接口,拥有执行命令的抽象方法execute()。
-
具体命令类(Concrete Command)角色:是抽象命令类的具体实现类,拥有接收者对象,并通过调用接收者的功能来完成命令要执行的操作。
-
实现者/接收者(Receiver)角色:执行命令功能的相关操作,是具体命令对象业务的真正实现者。
-
调用者/请求者(Invoker)角色:请求的发送者,通常拥有很多的命令对象,并通过访问命令对象来执行相关请求,它不直接访问接收者。
结构如下:
实现
案例:顾客去餐厅吃早餐,通过服务员点菜,服务员再通知厨师进行烹饪,整个过程中,顾客与厨师并未产生任何交流,通过点餐命令服务员角色进行传递,这里通过命令模式进行展示。这里:
- 点餐—相当于命令(例如:点包子、粥、河粉等)
- 服务员—相当于调用者
- 厨师—相当于接收者(包子厨师、粥厨师、河粉厨师)
- 定义抽象命令(早餐)
/**
*抽象命令类-声明执行命令的接口
*/
public abstract class AbsBreakfast {
public abstract void cooking();
}
- 定义具体命令对象(包子、河粉、油条)
/**
*具体命令对象-包子
*抽象命令对象的具体实现者,包含接收者对象,通过调用接收者对象的功能来实现命令要执行的操作
*/
public class BaoZi extends AbsBreakfast {
//接收者,具体命令对象业务真正实现者
BaoziChef baoziChef;
public BaoZi() {baoziChef = new BaoziChef();}
@Override
public void cooking() {baoziChef.cooking();}
}
/**
*具体命令对象-河粉
*/
public class HeFen extends AbsBreakfast {
//接收者,具体命令对象业务真正实现者
HeFenChef heFenChef;
public HeFen() {heFenChef = new HeFenChef();}
@Override
public void cooking() {heFenChef.cooking();}
}
/**
*具体命令对象-油条
*/
public class YouTiao extends AbsBreakfast{
//接收者,具体命令对象业务真正实现者
YouTiaoChef youTiaoChef;
public YouTiao(){ youTiaoChef = new YouTiaoChef();}
@Override
public void cooking() {youTiaoChef.cooking();}
}
- 定义接收者(厨师)
/**
*接收者-包子厨师(具体命令对象业务的真正实现者)
*/
public class BaoziChef {
public void cooking() {
System.out.println("我是包子厨师,我正在做包子");
}
}
/**
*接收者-河粉厨师(具体命令对象业务的真正实现者)
*/
public class HeFenChef {
public void cooking() {
System.out.println("我是河粉厨师,我正在做河粉");
}
}
/**
*接收者-油条厨师(具体命令对象业务的真正实现者)
*/
public class YouTiaoChef {
public void cooking() {
System.out.println("我是油条厨师,我正在炸油条");
}
}
- 定义调用者(服务员)
/**
*调用者-服务员(请求的发送者,通常拥有多个命令对象,并通过访问命令对象的来执行相关操作,不直接访问接收者)
*/
public class Waiter {
//拥有多个命令对象(抽象命令)
AbsBreakfast breakfast;
public void setBreakfast(AbsBreakfast breakfast){this.breakfast = breakfast;}
//通过调用命令对象来执行相关操作,不直接访问接收者
public void cooking(){breakfast.cooking();}
}
测试
//调用者-发送命令-接收者执行
Waiter waiter = new Waiter();
waiter.setBreakfast(new BaoZi());
waiter.cooking();
waiter.setBreakfast(new HeFen());
waiter.cooking();
//输出
我是包子厨师,我正在做包子
我是河粉厨师,我正在做河粉
使用场景
- 系统需要将
请求调用者
与请求接收者
解耦
时,命令模式使得调用者和接收者不直接交互。 - 系统需要随机请求命令或经常增加或删除命令时,命令模式比较方便实现。
- 系统需要执行一组操作时,命令模式可以定义宏命令来实现该功能。
- 当系统需要支持命令的撤销(Undo)操作和恢复(Redo)操作时,可以将命令对象存储起来,采用备忘录来实现。
扩展
1. 宏命令模式
命令模式+组合模式 = 宏命令模式
,也叫组合命令模式。宏命令包含一组命令,它充当了具体命令与调用者的双重角色,执行它时将递归调用它所包含的所有命令。
- 具体命令相当于树叶构件
- 调用者相当于树枝构件
结构如下:
其他不变,主要是调用者(树枝构件提供管理树叶构件功能)
//抽象命令
interface AbstractCommand
{
public abstract void execute();
}
//树叶构件: 具体命令1
class ConcreteCommand1 implements AbstractCommand
{
private CompositeReceiver receiver;
ConcreteCommand1()
{
receiver=new CompositeReceiver();
}
public void execute()
{
receiver.action1();
}
}
//树叶构件: 具体命令2
class ConcreteCommand2 implements AbstractCommand
{
private CompositeReceiver receiver;
ConcreteCommand2()
{
receiver=new CompositeReceiver();
}
public void execute()
{
receiver.action2();
}
}
//接收者
class CompositeReceiver
{
public void action1()
{
System.out.println("接收者的action1()方法被调用...");
}
public void action2()
{
System.out.println("接收者的action2()方法被调用...");
}
}
调用者-树枝构件
//树枝构件: 调用者-提供管理树叶构件的功能
class CompositeInvoker implements AbstractCommand
{
private ArrayList<AbstractCommand> children = new ArrayList<AbstractCommand>();
public void add(AbstractCommand c)
{
children.add(c);
}
public void remove(AbstractCommand c)
{
children.remove(c);
}
public AbstractCommand getChild(int i)
{
return children.get(i);
}
public void execute()
{
for(Object obj:children)
{
((AbstractCommand)obj).execute();
}
}
}
测试
AbstractCommand cmd1=new ConcreteCommand1();
AbstractCommand cmd2=new ConcreteCommand2();
CompositeInvoker ir=new CompositeInvoker();
ir.add(cmd1);
ir.add(cmd2);
System.out.println("客户访问调用者的execute()方法...");
ir.execute();