文章目录
本系列文章共分为六篇:
设计模式(一)设计模式的分类与区别
设计模式(二)创建型模式介绍及实例
设计模式(三)结构型模式介绍及实例
设计模式(四)行为型模式介绍及实例(上)
设计模式(五)行为型模式介绍及实例(下)
设计模式(六)设计模式的常见应用
上篇文件介绍了结构型模式,本篇将介绍行为型模式。行为型模式的主要关注点是“描述类或对象之间怎样通信、协作共同完成任务,以及怎样分配职责
”。因为行为型模式较多,分成上下两篇来介绍。
一、模板模式*
模板模式:定义一个操作中的算法骨架,而将算法的一些步骤延迟到子类中
,使得子类可以不改变该算法结构的情况下重定义该算法的某些特定步骤。即:父类定义固定的框架和公共部分,子类实现可变部分/步骤。
- 模板模式的优点
1、它封装了不变部分,扩展可变部分
。它把认为是不变部分的算法封装到父类中实现,而把可变部分算法由子类继承实现,便于子类继续扩展。
2、它在父类中提取了公共的部分代码
,便于代码复用。
3、部分方法是由子类实现的,因此子类可以通过扩展方式增加相应的功能,符合开闭原则。 - 模板模式的缺点
1、对每个不同的实现都需要定义一个子类,这会导致类的个数增加,系统更加庞大,设计也更加抽象。
2、父类中的抽象方法由子类实现,子类执行的结果会影响父类的结果,这导致一种反向的控制结构,它提高了代码阅读的难度。 - 模板模式角色
1、抽象类。负责给出一个算法的轮廓和骨架。它由一个模板方法和若干个基本方法构成。
模板方法:定义了算法的骨架,按某种顺序调用其包含的基本方法。为了防止恶意的操作,一般模板方法都加上final关键字,不允许被覆写
。
基本方法:是整个算法中的一个步骤,包含以下几种类型:
1>抽象方法:在抽象类中申明,由具体子类实现。
2>具体方法:在抽象类中已经实现,在具体子类中可以继承或重写它。
3>钩子方法
:在抽象类中已经实现,包括用于判断的逻辑方法和需要子类重写的空方法两种。
2、具体子类。实现抽象类中所定义的抽象方法和钩子方法,它们是一个顶级逻辑的一个组成步骤。
1.1 模板模式实现方式
以两个人张三和李四上班为例,主要涉及三个部分:起床、吃早饭、乘坐交通工具去上班,起床是公共方法,早饭的内容可以不同,乘什么样的交通工具可以用钩子方法来判别。示例代码:
/*抽象类*/
abstract class Worker {
public void WorkerWay() {
getUp();
haveBreakfast();
if(!haveCar()){
takeBus();
}else{
drive();
}
}
/*具体方法*/
public void getUp() {
System.out.println("起床");
}
public void takeBus(){
System.out.println("坐公交上班");
}
public void drive(){
System.out.println("开车上班");
}
/*钩子方法*/
public boolean haveCar(){ return true; }
/*抽象方法*/
public abstract void haveBreakfast();
}
/*具体子类:张三*/
public class Zhangsan extends Worker{
@Override
public void haveBreakfast() {
System.out.println("吃三明治、喝牛奶");
}
public boolean haveCar(){ return false; }
}
/*具体子类:李四*/
public class Lisi extends Worker{
@Override
public void haveBreakfast() {
System.out.println("吃包子、喝豆浆");
}
public boolean haveCar(){ return true; }
}
/*测试类*/
public class TemplateTest {
public static void main(String[] args) {
Zhangsan zhangsan =new Zhangsan();
System.out.println("张三的上班方式:");
zhangsan.WorkerWay();
Lisi lisi =new Lisi();
System.out.println("李四的上班方式:");
lisi.WorkerWay();
}
}
结果:
张三的上班方式:
起床
吃三明治、喝牛奶
开车上班
李四的上班方式:
起床
吃包子、喝豆浆
坐公交上班
1.2 模板模式应用场景
算法的整体步骤很固定,但其中个别部分易变
时,这时候可以使用模板方法模式,可以把核心算法设计为模板方法,周边的相关细节功能则由各个子类实现。当多个子类存在公共的行为时,可以将其提取出来并集中到一个公共父类中以避免代码重复
。首先,要识别现有代码中的不同之处,并且将不同之处分离为新的操作。最后,用一个调用这些新的操作的模板方法来替换这些不同的代码。- 当需要控制子类的扩展时,模板方法只在特定点调用钩子操作,这样就只允许在这些点进行扩展。
- 重构时,模板方法模式是一个经常使用的模式,把相同的代码抽取到父类中,然后通过钩子函数(见“模板方法模式的扩展”)约束其行为。
二、策略模式*
策略模式:该模式定义了一系列算法,并将每个算法封装起来,使它们可以相互替换
,且算法的变化不会影响使用算法的客户。即:有多种算法,供客户端替换使用。
- 策略模式角色
1、环境类(Context):用一个ConcreteStrategy对象来配置。维护一个对Strategy对象的引用。可以定义一个接口来让Strategy访问他的数据。
2、抽象策略类(Strategy):定义所有支持算法的公共接口。Context使用这个接口来调用某ConcreteStrategy定义的算法。
3、具体策略类(ConcreteStrategy):以Strategy接口实现具体的算法。
注意事项:具体策略数量超过 4 个,则需要考虑使用混合模式。
- 策略模式的优点
1、多重条件语句不易维护,而使用策略模式可以避免使用多重条件语句。策略模式提供了一系列的可供重用的算法族,恰当使用继承可以把算法族的公共代码转移到父类里面,从而避免重复的代码。
2、可以提供相同行为的不同实现,客户可以根据不同时间或空间要求选择不同的。
3、提供了对开闭原则的完美支持,可以在不修改原代码的情况下,灵活增加新算法。
4、把算法的使用放到环境类中,而算法的实现移到具体策略类中,实现了二者的分离。 - 策略模式的缺点
1、客户端必须理解所有策略算法的区别,以便适时选择恰当的算法类。
2、策略模式造成很多的策略类。
2.1 策略模式实现方式
以不同快递公司有不同的运费为例。计费方式为抽象策略,具体的快递公司的计费方式为具体策略。示例:
//抽象类
public interface CommandStrategy {
//计费方式
void calMoney(String message);
}
//不同的策略类
public class BaiShiCommand implements CommandStrategy {
//百世快递计费方式
@Override
public void calMoney(String message) {
System.out.println("百世快递收费方式:"+"起步20,每公斤6元");
}
}
public class JingDongCommand implements CommandStrategy {
/京东快递计费方式
@Override
public void calMoney(String message) {
System.out.println("京东快递收费方式:"+"起步30,每公斤5元");
}
}
public class YuanTongCommand implements CommandStrategy {
//圆通快递计费方式
@Override
public void calMoney(String message) {
System.out.println("圆通快递收费方式:"+"起步10,每公斤8元");
}
}
//环境类
public class CommandContext {
public CommandStrategy getInstance(String commandType) {
CommandStrategy commandStrategy = null;
Map<String, String> allClazz = CommandEnum.getAllClazz();
//拿到对应算法类对应的路径
String clazz = allClazz.get(commandType.trim().toLowerCase());
if (StringUtils.isNotEmpty(clazz)) {
try {
try {
//创建一个对象实例
commandStrategy = (CommandStrategy) Class.forName(clazz).newInstance();//调用无参构造器创建实例
} catch (InstantiationException | IllegalAccessException e) {
e.printStackTrace();
}
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
System.out.println("commandStrategy:"+commandStrategy);
return commandStrategy;
}
}
//不同策略的枚举
public enum CommandEnum {
JingDong("京东", "com.test.design.command.JingDongCommand"),
BaiShi("百世", "com.test.design.command.BaishiCommand"),
YuanTong("圆通", "com.test.design.command.YuanTongCommand");
private String name;
private String clazz;
public static Map<String, String> getAllClazz() {
Map<String, String> map = new HashMap<>(8);
System.out.println("==================="+Arrays.toString(CommandEnum.values())+"================");
for (CommandEnum commandEnum : CommandEnum.values()) {
map.put(commandEnum.getCommand(), commandEnum.getClazz());
}
return map;
}
public String getCommand() {
return name;
}
CommandEnum(String command, String clazz) {
this.name = command;
this.clazz = clazz;
}
public void setCommand(String command) {
this.name = command;
}
public String getClazz() {
return clazz;
}
public void setClazz(String clazz) {
this.clazz = clazz;
}
}
//测试类
public class MainStart {
public static void main(String[] args) {
String message = "京东";
CommandContext commandContext = new CommandContext();
//拿到message对应算法的对象实例
CommandStrategy commandStrategy = commandContext.getInstance(message);
commandStrategy.calMoney(message);
}
}
2.2 策略模式应用场景
- 一个系统需要
动态地在几种算法中选择一种
时,可将每个算法封装到策略类中。 - 一个类定义了多种行为,并且这些行为在这个类的操作中以多个条件语句的形式出现,可将每个条件分支移入它们各自的策略类中以代替这些条件语句。
- 系统中
各算法彼此完全独立,且要求对客户隐藏具体算法的实现细节
时。 - 系统要求使用算法的客户不应该知道其操作的数据时,可使用策略模式来隐藏与算法相关的数据结构。
- 多个类的区别仅仅在表现行为不同,可以使用策略模式,在
运行时动态选择具体要执行的行为
。 - 算法需要自由切换的场景。
三、状态模式
状态模式:对有状态的对象,把复杂的“判断逻辑”提取到不同的状态对象中,允许状态对象在其内部状态发生改变时改变其行为。即:对象的行为依赖于其状态,将状态提取为对象,降低对象之间的耦合。
-
状态模式主要角色
1、State类是抽象状态类,定义一个接口以封装与Context的一个特定状态相关的行为。
2、ConcreteState是具体的状态类,每一个子类实现一个与Context的一个状态相关的行为。
3、Context类,维护一个ConcreteState子类的实例,这个实例定义当前的状态。 -
状态模式的优点
1、状态模式将与特定状态相关的行为局部化到一个状态中,并且将不同状态的行为分割开来,满足“单一职责原则”。
2、减少对象间的相互依赖
。将不同的状态引入独立的对象中会使得状态转换变得更加明确,且减少对象间的相互依赖。
3、有利于程序的扩展。通过定义新的子类很容易地增加新的状态和转换。 -
状态模式的缺点
1、状态模式的使用必然会增加系统的类与对象的个数。
2、状态模式的结构与实现都较为复杂,如果使用不当会导致程序结构和代码的混乱。
3.1 状态模式实现方式
此处,以档位切换为例,某个开关有3个档位,且遵从高 --> 中 --> 低 --> 高的切换方式。示例代码如下:
/*抽象状态*/
public abstract class Switch {
public abstract void setPre(Context context);
public abstract void setNext(Context context);
}
/*具体状态1:低档位*/
public class LowSwitch extends Switch{
@Override
public void setPre(Context context) {
System.out.println("当前是低档位,向前切换为中档位");
context.setSwitch(new MiddSwitch());
}
@Override
public void setNext(Context context) {
System.out.println("当前是低档位,向后切换为高档位");
context.setSwitch(new HighSwitch());
}
@Override
public String toString() { return "低档位"; }
}
/*具体状态2:中档位*/
public class MiddSwitch extends Switch{
@Override
public void setPre(Context context) {
System.out.println("当前是中档位,向前切换为高档位");
context.setSwitch(new HighSwitch());
}
@Override
public void setNext(Context context) {
System.out.println("当前是中档位,向后切换为低档位");
context.setSwitch(new LowSwitch());
}
}
/*具体状态3:高档位*/
public class HighSwitch extends Switch{
@Override
public void setPre(Context context) {
System.out.println("当前是高档位,向前切换为低档位");
context.setSwitch(new LowSwitch());
}
@Override
public void setNext(Context context) {
System.out.println("当前是高档位,向后切换为中档位");
context.setSwitch(new MiddSwitch());
}
}
/*环境类*/
public class Context {
private Switch switch1;
//定义环境类的初始状态
public Context(){
this.switch1 = new LowSwitch();
}
//设置档位
public void setSwitch(Switch sw){
switch1 = sw;
}
//读取档位
public Switch getSwitch(){
System.out.println("当前档位是:"+switch1.toString());
return(switch1);
}
//往前设置一个档位
public void setPre(){
switch1.setPre(this);
}
//往后设置一个档位
public void setNext(){
switch1.setNext(this);
}
}
/*测试类*/
public class SwtichTest {
public static void main(String[] args){
Context context=new Context(); //创建环境
context.getSwitch(); //处理请求
context.setPre();
context.setPre();
context.setNext();
}
}
结果如下:
当前档位是:低档位
当前是低档位,向前切换为中档位
当前是中档位,向前切换为高档位
当前是高档位,向后切换为中档位
3.2 状态模式应用场景
- 当
一个对象的行为取决于它的状态
,并且它必须在运行时根据状态改变它的行为时,就可以考虑使用状态模式。即行为随状态改变而改变的场景,这也是状态模式的根本出发点,例如权限设计,人员的状态不同即使执行相同的行为结果也会不同,在这种情况下需要考虑使用状态模式。 - 一个操作中含有庞大的分支结构,并且这些分支决定于对象的状态时。
- 条件、分支判断语句的替代者。
注意:状态模式适用于当某个对象在它的状态发生改变时,它的行为也随着发生比较大的变化,也就是说在行为受状态约束的情况下可以使用状态模式,而且使用时对象的状态最好不要超过 5 个。
四、观察者模式
观察者模式:指多个对象间存在一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并被自动更新
。这种模式有时又称作发布-订阅模式、模型-视图模式,它是对象行为型模式。即:一个对象变化了,要通知到别的对象,以便处理这种变化。
在实际环境中,观察者常常和责任链模式一起使用,共同负责对事件的处理,其中某个观察者负责是否将事件进一步传递
。
-
观察者模式主要角色
1、Subject类,抽象通知者(或主题),一般用抽象类或接口实现。它把所有对观察者对象的引用保存在一个集合里,每个主题都可以有任何数量的观察者。主题中至少应包含三类方法:添加观察者、删除观察者和通知
。
2、ConcreteSubject类,叫做具体主题或具体通知者,将有关状态存入具体观察者对象。在具体主题的内部状态改变时,给所有登记过的观察者发出通知。具体主题通常用一个具体子类实现。
3、Observer类,抽象观察者,为所有的具体观察者定义一个接口,在得到主题的通知时更新自己,重选ing观察者一般用一个抽象类或一个接口实现。
4、ConcreteObserver类,具体观察者,实现抽象观察者角色所要求的的更新接口,以便使本身的状态和主体的状态相协调。具体观察者角色通常用一个具体子类实现。 -
观察者模式的优点
1、降低了目标与观察者之间的耦合
关系,两者之间是抽象耦合关系。
2、目标与观察者之间建立了一套触发
机制。 -
观察者模式的缺点
1、目标与观察者之间的依赖关系并没有完全解除,而且有可能出现循环引用。
2、当观察者对象很多时,通知的发布会花费很多时间,影响程序的效率。
观察者比较多,而且处理时间比较长时,采用异步处理来考虑线程安全和队列的问题。
4.1 观察者模式实现方式
以小明追小说为例,有两本小说《剑来》和《英雄志》都未完结,他关注了这两部小说,当则两部小说更新时,他都会收到对应的消息。在这个例子中,小说是抽象主题,不同的小说是具体主题。小说的阅读者是抽象观察者,具体阅读者小明是具体观察者。
代码示例:
/*抽象主题:小说*/
public abstract class Story {
protected List<Observer> observers=new ArrayList<Observer>();
//增加观察者方法
public void add(Observer observer){
observers.add(observer);
}
//删除观察者方法
public void remove(Observer observer){
observers.remove(observer);
}
public abstract void notifyObserver(int chapterNum); //通知观察者方法
}
/*具体主题1:剑来*/
public class Story1 extends Story{
private String name = "jianlai";
public void notifyObserver(int chapterNum){
for(Object obs:observers){
((Observer)obs).response(this.name,chapterNum);
}
}
}
/*具体主题2:英雄志*/
public class Story2 extends Story{
private String name = "yingxiongzhi";
public void notifyObserver(int chapterNum){
for(Object obs:observers){
((Observer)obs).response(this.name,chapterNum);
}
}
}
/*抽象观察者*/
public interface Observer {
void response(String storyName,int number);
}
/*具体观察者:小明*/
public class ObserverXiaoming implements Observer{
private String name = "小明";
public void response(String storyName,int chapterNum) {
if("jianlai".equals(storyName)){
System.out.println("《剑来》更新了"+chapterNum+"章,"+this.name+"等养肥了在看");
}else{
System.out.println("《英雄志》更新了"+chapterNum+"章,"+this.name+"立马去看");
}
}
}
/*测试类*/
public class ObserverTest {
public static void main(String[] args){
Story story1=new Story1();
Story story2=new Story2();
Observer observer=new ObserverXiaoming();
story1.add(observer);
story2.add(observer);
story1.notifyObserver(20);
story2.notifyObserver(10);
}
}
示例结果:
《剑来》更新了20章,小明等养肥了在看
《英雄志》更新了10章,小明立马去看
4.2 观察者模式应用场景
- 对象间存在一对多关系,
一个对象的状态发生改变会影响其他对象
。 - 当一个抽象模型有两个方面,其中一个方面依赖于另一方面时,可将这二者封装在独立的对象中以使它们可以各自独立地改变和复用。
- 关联行为场景。需要注意的是,关联行为是可拆分的,而不是“组合”关系。
- 事件多级触发场景。
- 跨系统的消息交换场景,如消息队列的处理机制。
五、备忘录模式
备忘录模式:在不破坏封装性的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态,以便以后当需要时能将该对象恢复到原先保存的状态
。该模式又叫快照
模式。即:可以保存和撤销对象行为的模式。
- 备忘录模式角色
1、源发器(Originator):需要保存和恢复状态的对象。它创建一个备忘录对象,用于存储当前对象的状态,也可以使用备忘录对象恢复自身的状态。
2、备忘录(Memento):存储源发器对象的状态。备忘录对象可以包括一个或多个状态属性,源发器可以根据需要保存和恢复状态。
3、管理者(Caretaker):负责保存备忘录对象,但不能修改备忘录对象的内容。它可以存储多个备忘录对象,并决定何时将备忘录恢复给源发器。 - 备忘录模式的优点
1、提供了一种可以恢复状态
的机制。当用户需要时能够比较方便地将数据恢复到某个历史的状态。
2、实现了内部状态的封装。除了创建它的发起人之外,其他对象都不能够访问这些状态信息。
3、简化了发起人。发起人不需要管理和保存其内部状态的各个备份,所有状态信息都保存在备忘录中,并由管理者进行管理,这符合单一职责原则。 - 备忘录模式的缺点
资源消耗大。如果要保存的内部状态信息过多或者特别频繁,将会占用比较大的内存资源。
5.1 备忘录模式实现方式
以文本编辑器为例,用户可以在编辑器中输入文本,并允许回退到之前的编辑状态。其中Editor类代表文本编辑器(源发器),它具有保存和恢复文本内容的功能。EditorMemento类表示备忘录对象,用于保存Editor的状态。History类作为管理者,负责保存和管理备忘录对象。示例代码:
//备忘录(Memento):保存编辑器的状态
//存储源发器对象的状态。备忘录对象可以包括一个或多个状态属性,源发器可以根据需要保存和恢复状态
public class EditorMemento {
//文本内容 (可以是属性、也可以是对象)
private String content;
public EditorMemento(String content) {
this.content = content;
}
public String getContent() {
return content;
}
}
//源发器(Originator):文本编辑器
//定义:需要保存和恢复状态的对象。它创建一个备忘录对象,用于存储当前对象的状态,也可以使用备忘录对象恢复自身的状态。
public class Editor {
//内容(可以是属性、也可以是对象)
private String content;
public void setContent(String content) {
this.content = content;
}
public String getContent() {
return content;
}
// 创建备忘录对象,保存当前状态
public EditorMemento createMemento() {
return new EditorMemento(content);
}
// 恢复备忘录对象保存的状态
public void restoreMemento(EditorMemento memento) {
content = memento.getContent();
}
}
//管理者(Caretaker):历史记录
//定义:负责保存备忘录对象,但不能修改备忘录对象的内容。它可以存储多个备忘录对象,并决定何时将备忘录恢复给源发器。
public class History {
//备忘录可以保存多个状态
private final List<EditorMemento> mementos = new ArrayList<>();
// 保存备忘录对象的状态
public void push(EditorMemento memento) {
mementos.add(memento);
}
// 弹出(移除)最近保存的备忘录对象,并返回它
public EditorMemento pop() {
int lastIndex = mementos.size() - 1;
EditorMemento lastMemento = mementos.get(lastIndex);
mementos.remove(lastIndex);
return lastMemento;
}
}
//测试类
public class TestMemento {
@Test
void testMemento(){
//创建文本编辑器(源发器)
Editor editor = new Editor();
//创建管理者
History history = new History();
// 编辑文本并保存状态
editor.setContent("Hello");
history.push(editor.createMemento());
// 编辑更多文本并再次保存状态
editor.setContent("Hello, Java!");
history.push(editor.createMemento());
editor.setContent("Hello,world!");
System.out.println("当前内容: " + editor.getContent());
// 恢复之前的状态
editor.restoreMemento(history.pop());
System.out.println("恢复后上一次内容: " + editor.getContent());
editor.restoreMemento(history.pop());
System.out.println("恢复后上二次内容: " + editor.getContent());
}
}
结果:
当前内容:Hello,world!
恢复后上一次内容:Hello,Java!
恢复后第二次内容:Hello
5.2 备忘录模式应用场景
- 需要
保存与恢复数据
的场景,如玩游戏时的中间结果的存档功能。 - 需要提供一个
可回滚操作
的场景,如Word、记事本、Photoshop,Eclipse等软件在编辑时按Ctrl+Z组合键,还有数据库中事务操作。 - 数据库连接的事务管理就是用的备忘录模式。
- 需要监控的副本场景中。