当前位置:   article > 正文

设计模式——行为型模式_行为模式描述了对象和类的模式

行为模式描述了对象和类的模式

行为型模式

行为型模式主要是用于描述类或者对象是怎样交互和怎样分配职责的。它涉及到算法和对象间的职责分配,不仅描述对象或者类的模式,还描述了他们之间的通信方式,它将你的注意力从控制流转移到了对象间的关系上来。行为型类模式采用继承机制在类间分派行为,而行为型对象模式使用对象复合而不是继承。它主要包括以下11种设计模式:职责链模式、命令模式、解释器模式、迭代器模式、中介者模式、备忘录模式、观察者模式、状态模式、策略模式、模板方法模式、访问者模式。

十二、职责链模式

避免请求发送者与接收者耦合在一起,让多个对象都有可能接收请求,将这些对象连接成一条链,并且沿着这条链传递请求,直到有对象处理它为止,这就是职责链模式。

12.1 前言

  • 学生请假

    image-20200913091349548

    学校规定所以去参加校招的必须要请假,且必须要有相关人员的签字,三天一下,辅导员签字、三到七天系主任签字,一个星期以上院长签字。对于这种将请求一级一级地往上传递直到处理请求为止的设计模式就是职责链模式。

  • 网络数据传输

    在计算机软硬件中也有相关例子,如总线网中数据报传送,每台计算机根据目标地址是否同自己的地址相同来决定是否接收;还有异常处理中,处理程序根据异常的类型决定自己是否处理该异常;还有 Struts2 的拦截器、JSP 和 Servlet 的 Filter 等,所有这些,如果用责任链模式都能很好解决。

12.2 概述

避免请求发送者与接收者耦合在一起,让多个对象都有可能接收请求,将这些对象连接成一条链,并且沿着这条链传递请求,直到有对象处理它为止,这就是职责链模式。

在职责链模式中最关键的一点就是客户提交请求后,请求沿着链往下传递直到有一个处理者处理它,在这里客户无需关心它的请求是哪个处理者来处理,反正总有一个处理者会处理它的请求。

在这里客户端和处理者都没有对方明确的信息,同时处理者也不知道职责链中的结构。所以职责链可以简化对象的相互连接,他们只需要保存一个指向其后续者的引用,而不需要保存所有候选者的引用。

在职责链模式中我们可以随时随地的增加或者更改一个处理者,甚至可以更改处理者的顺序,增加了系统的灵活性。处理灵活性是增加了,但是有时候可能会导致一个请求无论如何也得不到处理,它会被放置在链末端,这个既是职责链的优点也是缺点。

12.2.1 类图
image-20200913101257925
  • Handler:抽象处理者。定义了一个处理请求的方法,所有的处理者都必须实现该抽象类。它拥有一个handlerRequest方法,用来接收需要处理的请求。
  • ConcreteHandler:具体处理者。处理它所负责的请求,同时也可以访问它的后继者。如果它能够处理该请求则处理,否则将请求传递到它的后继者。每个具体处理器都维持一个引用,指向链中下一个具体处理器,需要检查它自身是否能处理这个请求,不能就将请求传递给链中下一个具体处理器。
  • Client:客户类。 客户端是使用责任链模式的应用程序的主要结构。它的职责是实例化一个处理器的链,然后在第一个对象种调用handlerRequest方法。
12.2.2 适用场景
  1. 有多个对象可以处理一个请求,哪个对象处理该请求由运行时刻自动确定。
  2. 可动态指定一组对象处理请求,或添加新的处理者。
  3. 在不明确指定请求处理者的情况下,向多个处理者中的一个提交请求。

12.3 实现

用责任链模式设计一个请假条审批模块:

假如规定学生请假小于或等于 2 天,班主任可以批准;小于或等于 7 天,系主任可以批准;小于或等于 10 天,院长可以批准;其他情况不予批准。

首先,定义一个领导类(Leader),它是抽象处理者,包含了一个指向下一位领导的指针 next 和一个处理假条的抽象处理方法 handleRequest(int LeaveDays);然后,定义班主任类(ClassAdviser)、系主任类(DepartmentHead)和院长类(Dean),它们是抽象处理者的子类,是具体处理者,必须根据自己的权力去实现父类的 handleRequest(int LeaveDays) 方法,如果无权处理就将假条交给下一位具体处理者,直到最后;客户类负责创建处理链,并将假条交给链头的具体处理者(班主任)。

image-20200913111717431
/**
 * 抽象处理器类
 * 领导类
 */
public abstract class Leader {

    // 设置职责链中的下一个处理器对象
    private Leader next;
    // 职责链的设置由客户端定义
    public void setNext(Leader next)
    {
        this.next=next;
    }
    public Leader getNext()
    {
        return next;
    }
    //处理请求的方法
    public abstract void handleRequest(int LeaveDays);

}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
/**
 * 具体处理器
 * 班主任类
 */
public class ClassAdviser extends Leader {

    @Override
    public void handleRequest(int LeaveDays) {
        if(LeaveDays<=2)
        {
            System.out.println("班主任批准您请假" + LeaveDays + "天。");
        }
        else
        {
            if(getNext() != null)
            {
                this.getNext().handleRequest(LeaveDays);
            }
            else
            {
                System.out.println("请假天数太多,没有人批准该假条!");
            }
        }
    }
}

/**
 * 具体处理器
 * 系主任类
 */
public class DepartmentHead extends Leader {
    @Override
    public void handleRequest(int LeaveDays) {
        if(LeaveDays<=7)
        {
            System.out.println("系主任批准您请假" + LeaveDays + "天。");
        }
        else
        {
            if(getNext() != null)
            {
                getNext().handleRequest(LeaveDays);
            }
            else
            {
                System.out.println("请假天数太多,没有人批准该假条!");
            }
        }
    }
}

/**
 * 具体处理器类
 * 院长类
 */
public class Dean extends Leader {
    @Override
    public void handleRequest(int LeaveDays) {
        if(LeaveDays<=10)
        {
            System.out.println("院长批准您请假" + LeaveDays + "天。");
        }
        else
        {
            if(getNext() != null)
            {
                getNext().handleRequest(LeaveDays);
            }
            else
            {
                System.out.println("请假天数太多,没有人批准该假条!");
            }
        }
    }
}

/**
 * 具体处理器
 * 教务处长
 */
public class DeanOfStudies extends Leader {
    @Override
    public void handleRequest(int LeaveDays) {
        if(LeaveDays<=20)
        {
            System.out.println("教务处长批准您请假"+LeaveDays+"天。");
        }
        else
        {
            if(getNext()!=null)
            {
                getNext().handleRequest(LeaveDays);
            }
            else
            {
                System.out.println("请假天数太多,没有人批准该假条!");
            }
        }
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
// 客户端
public class Client {

    public static void main(String[] args)
    {
        //组装责任链
        Leader teacher1=new ClassAdviser();
        Leader teacher2=new DepartmentHead();
        Leader teacher3=new Dean();
        //Leader teacher4=new DeanOfStudies();
        
        // 由客户端来自定义职责链
        teacher1.setNext(teacher2);
        teacher2.setNext(teacher3);
        //teacher3.setNext(teacher4);
        
        //提交请求
        teacher1.handleRequest(8);
    }

}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21

12.4 特点

在责任链模式中,客户只需要将请求发送到责任链上即可,无须关心请求的处理细节和请求的传递过程,所以责任链将请求的发送者和请求的处理者解耦了。

12.4.1 优点

责任链模式是一种对象行为型模式,其主要优点如下。

  1. 降低了对象之间的耦合度。该模式使得一个对象无须知道到底是哪一个对象处理其请求以及链的结构,发送者和接收者也无须拥有对方的明确信息。
  2. 增强了系统的可扩展性。可以根据需要增加新的请求处理类,满足开闭原则。
  3. 增强了给对象指派职责的灵活性。当工作流程发生变化,可以动态地改变链内的成员或者调动它们的次序,也可动态地新增或者删除责任。
  4. 责任链简化了对象之间的连接。每个对象只需保持一个指向其后继者的引用,不需保持其他所有处理者的引用,这避免了使用众多的 if 或者 if···else 语句。
  5. 责任分担。每个类只需要处理自己该处理的工作,不该处理的传递给下一个对象完成,明确各类的责任范围,符合类的单一职责原则。
12.4.2 缺点

其主要缺点如下。

  1. 不能保证每个请求一定被处理。由于一个请求没有明确的接收者,所以不能保证它一定会被处理,该请求可能一直传到链的末端都得不到处理。
  2. 对比较长的职责链,请求的处理可能涉及多个处理对象,系统性能将受到一定影响。
  3. 职责链建立的合理性要靠客户端来保证,增加了客户端的复杂性,可能会由于职责链的错误设置而导致系统出错,如可能会造成循环调用。

12.5 总结

  • 职责链模式将请求的发送者和接受者解耦了。客户端不需要知道请求处理者的明确信息,甚至不需要知道链的结构,它只需要将请求进行发送即可。

  • 职责链模式能够非常方便的动态增加新职责或者删除职责。

  • 客户端发送的请求可能会得不到处理。

  • 处理者不需要知道链的结构,只需要明白他的后续者是谁就可以了。这样就简化了系统中的对象。

十三、命令模式

命令模式:将一个请求封装为一个对象,使发出请求的责任和执行请求的责任分割开。这样两者之间通过命令对象进行沟通,这样方便将命令对象进行储存、传递、调用、增加与管理。

13.1 前言

在软件设计中,我们经常需要向某些对象发送请求,但是并不知道请求的接收者是谁,也不知道被请求的操作是哪个,我们只需在程序运行时指定具体的请求接收者即可,此时,可以使用命令模式来进行设计,使得请求发送者与请求接收者消除彼此之间的耦合,让对象之间的调用关系更加灵活。

在现实生活中,这样的例子也很多:

  • 电视机遥控器 : 遥控器是请求的发送者,电视机是请求的接收者,遥控器上有一些按钮如开,关,换频道等按钮就是具体命令,不同的按钮对应电视机的不同操作。

13.2 概述

命令模式将请求封装成对象,以便可用不同的请求对客户进行参数化,对请求排队或者记录请求日志,以及支持可撤销的操作。

命令模式可以对发送者额接受者完全解耦,发送者也接收者之间并没有直接的联系,发送者只需要知道如何发送请求,不需要关心请求是如何完成了。这就是命令模式,命令模式将方法调用给封装起来了。

13.2.1 类图

image-20200914105855096

  • **抽象命令类(Command):**声明执行操作的接口。调用接收者相应的操作,以实现执行的方法Execute。

  • **具体命令类(ConcreteCommand):**创建一个具体命令对象并设定它的接收者。通常会持有接收者,并调用接收者的功能来完成命令要执行的操作。

  • **调用者(Invoker):**要求该命令执行这个请求。通常会持有命令对象,可以持有很多的命令对象。这是触发命令的类,通常是外部事件,例如用户操作。

  • **接收者(Receiver):**知道如何实施与执行一个请求相关的操作。任何类都可能作为一个接收者,只要它能够实现命令要求实现的相应功能。 负责执行与命令关联的操作的类。

  • **客户类(Client):**创建具体的命令对象,并且设置命令对象的接收者。真正使用命令的客户端是从Invoker来触发执行。

命令模式的本质就在于将命令进行封装,将发出命令的责任和执行命令的责任分开,使得发送者只需要知道如何发送命令即可,不需要命令是如何实现的,甚至命令执行是否成功都不需要理会。同时命令模式使得请求也变成了一个对象,它像其他对象一样可以被存储和传递。

13.2.2 适用场景
  1. 当系统需要将请求调用者与请求接收者解耦时,命令模式使得调用者和接收者不直接交互。
  2. 当系统需要随机请求命令或经常增加或删除命令时,命令模式比较方便实现这些功能。
  3. 当系统需要执行一组操作时,命令模式可以定义宏命令来实现该功能。
  4. 当系统需要支持命令的撤销(Undo)操作和恢复(Redo)操作时,可以将命令对象存储起来,采用备忘录模式来实现。

13.3 实现

/**
 * 抽象命令类
 * 声明执行操作的接口
 * 定义execute函数,引导各个具体实现类应该执行哪个命令
 */
public interface Command {

    public void execute();

}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
/**
 * 具体命令类
 * 关联一个接收者对象,由于接收者内含有与命令有关的操作
 * 所以该类就实现了命令的发出与执行之间的解耦
 */
public class OpenTvCommand implements Command {

    // 关联一个接收者对象
    private Television tv;

    public OpenTvCommand() {
        tv = new Television();
    }

    @Override
    public void execute() {
        tv.open();
    }
}

public class ChangeChannelCommand implements Command {

    private Television tv;

    public ChangeChannelCommand() {
        this.tv = new Television();
    }

    @Override
    public void execute() {
        tv.changeChannel();
    }
}

public class CloseTvCommand implements Command {

    private Television tv;

    public CloseTvCommand() {
        this.tv = new Television();
    }

    @Override
    public void execute() {
        tv.close();
    }
}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
/**
 * 接收者类
 * 面向客户端的统一接口
 * 负责执行与命令关联的操作
 */
public class Television {

    public void open(){
        System.out.println("打开电视机......");
    }

    public void close(){
        System.out.println("关闭电视机......");
    }

    public void changeChannel(){

        System.out.println("切换电视频道......");
    }

}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
/**
 * 调用者
 * 触发命令的执行
 * 该类是一个外部类,要求该命令执行这个请求
 */
public class Controller {

    private Command command;

    public void setCommand(Command command) {
        this.command = command;
    }

    /**
     * 打开电视剧
     */
    public void open(){
        command.execute();
    }

    /**
     * 关闭电视机
     */
    public void close(){
        command.execute();
    }

    /**
     * 换频道
     */
    public void change(){

        command.execute();
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
public class Client {

    public static void main(String a[])
    {
        Command openCommand,closeCommand,changeCommand;

        // 定义具体命令
        openCommand = new OpenTvCommand();
        closeCommand = new CloseTvCommand();
        changeCommand = new ChangeChannelCommand();

        // 调用者
        Controller control = new Controller();
        // 调用者设置要执行的命令
        control.setCommand(openCommand);
        control.open();           //打开电视机

        control.setCommand(changeCommand);
        control.change();         //换频道

        control.setCommand(closeCommand);
        control.close();          //关闭电视机
    }

}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25

13.4 特点

13.4.1 优点
  • 降低系统的耦合度。命令模式能将调用操作的对象与实现该操作的对象解耦。

  • Command是头等的对象,增加或删除命令非常方便。采用命令模式增加与删除命令不会影响其他类,它满足“开闭原则”,对扩展比较灵活。

  • 可以实现宏命令。命令模式可以与组合模式结合,将多个命令装配成一个组合命令,即宏命令。

  • 方便实现 Undo 和 Redo 操作。命令模式可以与后面介绍的备忘录模式结合,实现命令的撤销与恢复。

宏命令又称为组合命令,它是命令模式和组合模式联用的产物:

宏命令也是一个具体命令,不过它包含了对其他命令对象的引用,在调用宏命令的execute()方法时,将递归调用它所包含的每个成员命令的execute()方法,一个宏命令的成员对象可以是简单命令,还可以继续是宏命令。执行一个宏命令将执行多个具体命令,从而实现对命令的批处理。

13.4.2 缺点

使用命令模式可能会导致某些系统有过多的具体命令类。因为针对每一个命令都需要设计一个具体命令类,因此某些系统可能需要大量具体命令类,这将影响命令模式的使用。

13.5 总结

1)命令模式的本质是对命令进行封装,将发出命令的责任和执行命令的责任分割开。

2)每一个命令都是一个操作:请求的一方发出请求,要求执行一个操作;接收的一方收到请求,并执行操作

3)命令模式允许请求的一方和接收的一方独立开来,使得请求的一方不必知道接收请求的一方的接口,更不必知道请求是怎么被接收,以及操作是否被执行、何时被执 行,以及是怎么被执行的。

4)命令模式使请求本身成为一个对象,这个对象和其他对象一样可以被存储和传递。

5)命令模式的关键在于引入了抽象命令接口,且发送者针对抽象命令接口编程,只有实现了抽象命令接口的具体命令才能与接收者相关联。

十四、解释器模式

给分析对象定义一个语言,并定义该语言的文法表示,再设计一个解析器来解释语言中的句子。也就是说,用编译语言的方式来分析应用中的实例。这种模式实现了文法表达式处理的接口,该接口解释一个特定的上下文。

14.1 前言

  • 在软件开发中,会遇到有些问题多次重复出现,而且有一定的相似性和规律性。如果将它们归纳成一种简单的语言,那么这些问题实例将是该语言的一些句子,这样就可以用“编译原理”中的解释器模式来实现了。
  • 为人处事是一门大学问,察言观色、听懂弦外之音都是非常重要的,老板跟你说“XX你最近表现平平啊,还得要多努力”,如果你不当回事,平常对待,可能下次就是“XX,恩,你人还是不错,平常工作也很努力,但是我想这份工作可能不是很适合你……”。又比如你老大说“XX,你最近表现不错,工作积极性很高啊!继续保持啊!”,你高兴乐呵着心想是不是老板要给我加工资了,可能你等到花都谢了也没有,得到的可能会是更多的工作量。对于我们刚刚入社会的人不够圆滑,不会察言观色,更听不懂老板的弦外之音,所以我们期待如果有一个翻译机该多好,直接将别人的弦外之音给翻译出来就好了。
  • 有个游戏,输入up walk 5,玩家必须按照:移动方向+移动方式+移动距离这种格式输入我的指令,而这种格式的指令就是一种文法,只有按照了我定义的这种文法去输入,才能控制屏幕上的小狗去移动。当然了,我输入up walk 5,屏幕上的小狗肯定是听不懂的,它不知道我输入的是什么,这个时候需要怎么办?我需要一个工具去将我输入的内容翻译成小狗能听懂的东西,而这个工具就是定义中提到的解释器,解释器对我输入的指令进行解释,然后将解释得到的指令发送给屏幕上的小狗,小狗听懂了,就进行实际的移动。

虽然使用解释器模式的实例不是很多,但对于满足以上特点,且对运行效率要求不是很高的应用实例,如果用解释器模式来实现,其效果是非常好的,本文将介绍其工作原理与使用方法。

14.2 概述

**给定一个语言,定义它的文法的一种表示,并定义一个解释器,这个解释器使用该表示来解释语言中的句子。**解释器模式描述了如何构成一个简单的语言解释器,主要应用在使用面向对象语言开发的编译器中。它描述了如何为简单的语言定义一个文法,如何在该语言中表示一个句子,以及如何解释这些句子。

14.2.1 文法与句子

文法是用于描述语言的语法结构的形式规则。

句子是语言的基本单位,是语言集中的一个元素,它由终结符构成,能由“文法”推导出。

〈句子〉::=〈主语〉〈谓语〉〈宾语〉
〈主语〉::=〈代词〉|〈名词〉
〈谓语〉::=〈动词〉
〈宾语〉::=〈代词〉|〈名词〉
〈代词〉::=你|我|他
〈名词〉::=大学生|筱霞|英语
〈动词〉::=是|学习
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

根据上述文法可以推导出句子为“我是大学生”。

注:这里的符号“::=”表示“定义为”的意思,用“〈”和“〉”括住的是非终结符,没有括住的是终结符。

14.2.2 抽象语法树

当有一个语言需要解释执行,并且你可将该语言中的句子表示为一个抽象语法树时,可使用解释器模式。而当存在以下情况时该模式效果最好:

  • 该文法简单对于复杂的文法,文法的类层次变得庞大而无法管理。

  • 效率不是一个关键问题最高效的解释器通常不是通过直接解释语法分析树实现的,而是首先将它们转换成另一种形式。

以上述的游戏例子为例,可以定义以下五条文法:

expression ::= direction action distance | composite //表达式
composite ::= expression 'and' expression //复合表达式
direction ::= 'up' | 'down' | 'left' | 'right' //移动方向
action ::= 'move' | 'walk' //移动方式
distance ::= an integer //移动距离
  • 1
  • 2
  • 3
  • 4
  • 5
  • 上面的5条文法规则,对应5个语言单位,这些语言单位可以分为两大类:一类为终结符(也叫做终结符表达式),例如上面的direction、action和distance,它们是语言的最小组成单位,不能再进行拆分;另一类为非终结符(也叫做非终结符表达式),例如上面的expression和composite,它们都是一个完整的句子,包含一系列终结符或非终结符。

  • 我们就是根据上面定义的一些文法可以构成更多复杂的语句,计算机程序将根据这些语句进行某种操作;而我们这里列出的文法,计算机是无法直接看懂的,所以,我们需要对我们定义的文法进行解释;就好比,我们编写的C++代码,计算机是看不懂的,我们需要进行编译一样。解释器模式,就提供一种模式去给计算机解释我们定义的文法,让计算机根据我们的文法去进行工作。

  • 文法规则定义中可以使用一些符号来表示不同的含义,如使用“|”表示或,使用“{”和“}”表示组合,使用“*”表示出现0次或多次等,其中使用频率最高的符号是表示“或”关系的“|”,如文法规则“bool Value ::= 0 | 1”表示终结符表达式bool Value的取值可以为0或者1。

  • 除了使用文法规则来定义一个语言,在解释器模式中还可以通过一种称之为抽象语法树的图形方式来直观地表示语言的构成,每一棵语法树对应一个语言实例,对于上面的游戏文法规则,可以通过以下的抽象语法树来进行表示:

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-KflMiiC2-1625839249477)(https://segmentfault.com/img/bVU2OR?w=893&h=629/view)]

14.2.3 类图

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-448JocBf-1625839249478)(E:\我的资料\study\upload\image-20200915091552297.png)]

  • **AbstractExpression(抽象表达式):**声明一个抽象的解释操作,这个接口为抽象语法树中所有的节点所共享

  • TerminalExpression(终结符表达式): 实现与文法中的终结符相关联的解释操作。一个句子中的每个终结符需要该类的一个实例

  • NonterminalExpression(非终结符表达式): 为文法中的非终结符实现解释操作

    • 对于文法中的每一条规则都需要一个NonternimalExpression类;
    • 为文法中的的每个符号都维护一个AbstractExpression类型的实例变量;
    • 为文法中的非终结符实现解释操作,在实现时,一般要递归地调用表示文法符号的那些对象的解释操作;
  • Context(上下文): 包含解释器之外的一些全局信息

  • **Client(客户):**构建表示该文法定义的语言中一个特定的句子的抽象语法树。 该抽象语法树由NonterminalExpression和TerminalExpression的实例装配而成。 即构建一个需要进行解释操作的文法句子,然后调用解释操作进行解释


实际进行解释时,按照以下时序进行的:

  • Client构建一个句子,它是NonterminalExpression和TerminalExpression的实例的一个抽象语法树,然后初始化上下文并调用解释操作;
  • 每一非终结符表达式节点定义相应子表达式的解释操作。而各终结符表达式的解释操作构成了递归的基础;
  • 每一节点的解释操作用上下文来存储和访问解释器的状态
14.2.4 适用场景
  • 如果在系统中某一特定类型的问题发生的频率很高,此时可以考虑将这些问题的实例表述为一个语言中的句子,因此可以构建一个解释器,该解释器通过解释这些句子来解决这些问题。
  • 可以将一个需要解释执行的语言中的句子表示为一个抽象语法树。
  • 一些重复出现的问题可以用一种简单的语言来进行表达。
  • 一个简单语法需要解释的场景。

14.3 实现

用解释器模式设计一个“韶粵通”公交车卡的读卡器程序。

说明:假如“韶粵通”公交车读卡器可以判断乘客的身份,如果是“韶关”或者“广州”的“老人” “妇女”“儿童”就可以免费乘车,其他人员乘车一次扣 2 元。

分析:本实例用“解释器模式”设计比较适合,首先设计其文法规则如下。

<expression> ::= <city>的<person>
<city> ::= 韶关|广州
<person> ::= 老人|妇女|儿童
  • 1
  • 2
  • 3

根据文法规则按以下步骤设计公交车卡的读卡器程序的类图。

  • 定义一个抽象表达式(Expression)接口,它包含了解释方法 interpret(String info)。
  • 定义一个终结符表达式(Terminal Expression)类,它用集合(Set)类来保存满足条件的城市或人,并实现抽象表达式接口中的解释方法 interpret(Stringinfo),用来判断被分析的字符串是否是集合中的终结符。
  • 定义一个非终结符表达式(AndExpressicm)类,它也是抽象表达式的子类,它包含满足条件的城市的终结符表达式对象和满足条件的人员的终结符表达式对象,并实现 interpret(String info) 方法,用来判断被分析的字符串是否是满足条件的城市中的满足条件的人员。
  • 最后,定义一个环境(Context)类,它包含解释器需要的数据,完成对终结符表达式的初始化,并定义一个方法 freeRide(String info) 调用表达式对象的解释方法来对被分析的字符串进行解释

其类图如下:

image-20200915095046289

/**
 * 抽象表达式类
 * 声明执行的解释方法
 */
public interface Expression {

    public boolean interpret(String info);

}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
/**
 * 终结符表达式类
 * 实现与文法中的终结符相关联的解释操作
 * 终结符中定义了最根本的数据的设置
 */
public class TerminalExpression implements Expression {

    // 定义存储原始数据的集合
    private Set<String> set = new HashSet<>();
    // 定义构造器,初始化文法的原始数据集
    public TerminalExpression(String[] data) {
        for (int i = 0; i < data.length; i++) {
            set.add(data[i]);
        }
    }

    // 解释该词是否存在于文法的原始数据中
    @Override
    public boolean interpret(String info) {

        if (set.contains(info))
            return true;
        return false;
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
/**
 * 非终结符表达式类
 * 为文法中的非终结符实现解释操作
 * 该类除了要实现抽象表达式类,其内部还维护有一个抽象表达式的对象
 */
public class NonterminalExpression implements Expression {

    // 聚合两个抽象表达式实例对象
    // 用于在实现抽象语法树的“遍历”
    private Expression city;
    private Expression people;

    public NonterminalExpression(Expression city, Expression people) {
        this.city = city;
        this.people = people;
    }

    @Override
    public boolean interpret(String info) {

        String s[] = info.split("的");

        return city.interpret(s[0])&&people.interpret(s[1]);
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
/**
 * 上下文类
 * 定义原始的数据
 * 是客户端调用的接口
 */
public class Context {

    // 原始数据
    private String[] citys={"韶关","广州"};
    private String[] persons={"老人","妇女","儿童"};

    private Expression cityPerson;

    public Context()
    {
        // 调用各个终结符与非终结符的构造器进行初始化
        Expression city=new TerminalExpression(citys);
        Expression person=new TerminalExpression(persons);

        // 非终结符中含有终结符的对象
        cityPerson=new NonterminalExpression(city,person);
    }
    public void freeRide(String info)
    {
        boolean ok=cityPerson.interpret(info);
        if(ok) System.out.println("您是"+info+",您本次乘车免费!");
        else System.out.println(info+",您不是免费人员,本次乘车扣费2元!");
    }

}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
/**
 * 客户端
 * 调用的是Context类
 */
public class Client {

    public static void main(String[] args)
    {
        Context bus=new Context();
        bus.freeRide("韶关的老人");
        bus.freeRide("韶关的年轻人");
        bus.freeRide("广州的妇女");
        bus.freeRide("广州的儿童");
        bus.freeRide("山东的儿童");
    }

}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17

14.4 特点

解释器(Interpreter)模式的定义:给分析对象定义一个语言,并定义该语言的文法表示,再设计一个解析器来解释语言中的句子。也就是说,用编译语言的方式来分析应用中的实例。这种模式实现了文法表达式处理的接口,该接口解释一个特定的上下文。

14.4.1 优点
  • 可扩展性比较好,灵活。

  • 增加了新的解释表达式的方式。

  • 易于实现文法。

14.4.2 缺点
  • 执行效率较低。解释器模式中通常使用大量的循环和递归调用,当要解释的句子较复杂时,其运行速度很慢,且代码的调试过程也比较麻烦。
  • 会引起类膨胀。解释器模式中的每条规则至少需要定义一个类,当包含的文法规则很多时,类的个数将急剧增加,导致系统难以管理与维护。
  • 可应用的场景比较少。在软件开发中,需要定义语言文法的应用实例非常少,所以这种模式很少被使用到。

14.5 总结

  • 在解释器模式中由于语法是由很多类表示的,所以可扩展性强。

  • 虽然解释器的可扩展性强,但是如果语法规则的数目太大的时候,该模式可能就会变得异常复杂。所以解释器模式适用于文法较为简单的。

  • 解释器模式可以处理脚本语言和编程语言。常用于解决某一特定类型的问题频繁发生情况。

十五、迭代器模式

迭代器模式提供了一个对象来顺序访问聚合对象中的一系列数据,而不暴露聚合对象的内部表示。

15.1 前言

类中的面向对象编程封装应用逻辑。类,就是实例化的对象,每个单独的对象都有一个特定的身份和状态。单独的对象是一种组织代码的有用方法,但通常你会处理一组对象或者集合。但是集合不一定是均一的。

对电视机的电视频道、电影和收音机菜单进行统一管理,建立一个统一的菜单管理界面,能够看到所有的电视界面、电影界面和收音机频道。你有三个手下:小李子、小杏子、小安子,他们分别就每个模块做开发工作,看他们都做了哪些工作。

image-20200916203358896

由于这三个部分的代码使用了不同的数据类型,所以在对菜单选项做遍历操作时,就会不太方便。此时可以使用封装遍历的方式。这种方式能够游走于聚合内的每一个元素,同时还可以提供多种不同的遍历方式。

在实际的开发过程中,我们可能需要针对不同的需求,可能需要以不同的方式来遍历整个聚合对象,但是我们不希望在聚合对象的抽象接口层中充斥着各种不同的遍历操作。这个时候我们就需要这样一种机制,它应该具备如下三个功能:

  • 能够遍历一个聚合对象。

  • 我们不需要了解聚合对象的内部结构。

  • 能够提供多种不同的遍历方式。

这三个功能就是迭代器模式需要解决的问题。作为一个功能强大的模式,迭代器模式把在元素之间游走的责任交给迭代器,而不是聚合对象。这样做就简化了聚合的接口和实现,也可以让聚合更专注在它所应该专注的事情上,这样做就更加符合单一责任原则

15.2 概述

在软件构建过程中,集合对象内部结构常常变化各异,但对于这些集合对象,我们希望在不暴露其内部结构的同时,可以让外部客户代码透明地访问其中包含的元素;同时这种“透明遍历”也为同一种算法在多种集合对象上进行操作提供了可能。

使用面向对象技术将这种遍历机制抽象为“迭代器对象”为“应对变化中的集合对象”提供了一种优雅的方式。迭代器模式提供了一种顺序遍历聚合对象而不暴露其内部实现的方法。

15.2.1 聚集

多个对象聚在一起形成的总体称之为聚集(Aggregate),聚集对象是能够包容一组对象的容器对象。聚集依赖于聚集结构的抽象化,具有复杂性和多样性,数组就是最基本的聚集,也是其他Java聚集对象的设计基础。

**聚集对象必须提供适当的方法,允许客户端按照一个线性顺序遍历所有元素对象,把元素对象提取出来或者删除等。**一个使用聚集的系统必然会使用这些方法操作聚集对象,因而在使用聚集的系统演化过程中,会出现两类情况。

  • 迭代逻辑没有改变,但是需要将一种聚集对象换成另一种聚集,因为不同的聚集具有不同的遍历接口,所以需要修改客户端代码,以便将已有的迭代调用换成新聚集对象所要求的接口。

  • 聚集不改变,但是迭代方式需要改变,比如原来只需要读取元素和删除元素,但现在需要增加新的;或者原来的迭代仅仅遍历所有的元素,而现在则需要对元素加以过滤等。这时就只好修改聚集对象,修改已有的遍历方法,或者增加新的方法。

显然,出现这种情况是因为所涉及的聚集设计不符合“开-闭”原则,也就是因为没有将不变的结构从系统中抽象出来,与可变成分分割,并将可变部分的各种实现封装起来。一个聪明的做法无疑是应使用更加抽象的处理方法,使得在进行迭代时,客户端根本无需知道所使用的聚集是哪个类型;而当客户端需要使用全新的迭代逻辑时,只需要引进一个新的迭代子对象即可,根本无需修改聚集对象本身。

迭代模式便是这样的一个抽象化的概念,这一模式之所以能够做到这一点,是因为它将迭代逻辑封装到一个独立的迭代子对象汇总,从而与聚集本身分隔开。迭代子对象是对遍历的抽象化,不同的聚集对象可以提供相同的迭代子对象,从而使客户端无需知道聚集的底层结构,一个聚集可以提供多个不同的迭代子对象,从而使得遍历逻辑的变化不会影响到聚集对象本身。

15.2.2 类图

迭代器模式是通过将聚合对象的遍历行为分离出来,抽象成迭代器类来实现的,其目的是在不暴露聚合对象的内部结构的情况下,让外部代码透明地访问聚合的内部数据。

image-20200916205159131

  • Iterator: 抽象迭代器。所有迭代器都需要实现的接口,提供了游走聚合对象元素之间的方法,通常包含 hasNext()、first()、next() 等方法。

  • ConcreteIterator: 具体迭代器。利用这个具体的迭代器能够对具体的聚合对象进行遍历。每一个聚合对象都应该对应一个具体的迭代器。

  • Aggregate: 抽象聚合类。定义存储、添加、删除聚合对象以及创建迭代器对象的接口

  • ConcreteAggregate: 具体聚合类。实现创建相应迭代器接口的方法,返回一个具体迭代器的实例。

15.2.3 适用场景
  • 访问一个聚合对象的内容而无须暴露它的内部表示。

  • 需要为聚合对象提供多种遍历方式。

  • 为遍历不同的聚合结构提供一个统一的接口。

15.3 实现

对电视机的电视频道、电影和收音机菜单进行统一管理,建立一个统一的菜单管理界面,能够看到所有的电视界面、电影界面和收音机频道。你有三个手下:小李子、小杏子、小安子,他们分别就每个模块做开发工作,分别使用不同的数据类型。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-pxXNM50d-1625839249479)(upload/image-20200917090650934.png)]

  • 外部类:主菜单类,内部包含有多个子菜单项:电影、电视子菜单….
/**
 * 外部类
 * 主菜单类
 * 内含多个两个子菜单对象:电影菜单与电视节目菜单
 */
public class MainMenu {
    TelevisionMenu tvMenu;
    FilmMenu filmMenu;

    public MainMenu(TelevisionMenu tvMenu,FilmMenu filmMenu){
        this.tvMenu = tvMenu;
        this.filmMenu  = filmMenu;
    }

    public void printMenu(){
        Iterator tvIterator = tvMenu.createIterator();
        Iterator filmIterator = filmMenu.createIterator();

        System.out.println("电视节目有:");
        printMenu(tvIterator);
        System.out.println("-----------------------------------");
        System.out.println("电影节目有:");
        printMenu(filmIterator);
    }

    private void printMenu(Iterator iterator) {
        while(iterator.hasNext()){
            MenuItem menuItem = (MenuItem) iterator.next();
            System.out.print("channel:"+menuItem.getChannel()+",  ");
            System.out.print("name:"+menuItem.getName()+",  ");
            System.out.println("description :"+menuItem.getDescription());
        }
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
/**
 * 主菜单子对象
 * 用于将不同数据类型的数据统一起来
 */
public class MenuItem {
    private String name;
    private String description;
    private int channel;

    public MenuItem(int channel,String name,String description){
        this.name = name;
        this.description = description;
        this.channel = channel;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getDescription() {
        return description;
    }

    public void setDescription(String description) {
        this.description = description;
    }

    public int getChannel() {
        return channel;
    }

    public void setChannel(int channel) {
        this.channel = channel;
    }

}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 抽象迭代器:实现遍历操作
/**
 * 抽象迭代器
 * 定义游走在各个聚合对象之间的方法
 */
public interface Iterator {

    boolean hasNext();
    Object next();

}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 具体迭代器:
/**
 * 具体迭代器
 * 对具体的聚合对象进行遍历
 */
public class FilmMenuIterator implements Iterator {

    // 关联一个主菜单对象
    MenuItem[] menuItems;
    int position = 0;
    
    // 数组形式
    public FilmMenuIterator(MenuItem[] menuItems) {
        this.menuItems = menuItems;
    }
	
    // 由于各部分的存储数据类型不同,其遍历的操作也不同
    @Override
    public boolean hasNext() {
        if(position > menuItems.length-1 || menuItems[position] == null){
            return false;
        }
        return true;
    }

    @Override
    public Object next() {
        MenuItem menuItem = menuItems[position];
        position ++;
        return menuItem;
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
/**
 * 具体迭代器
 * 对具体的聚合对象进行遍历
 */
public class TVChanneMenuIterator implements Iterator {

    // 关联一个主菜单对象,使用List
    List<MenuItem> menuItems;
    int position = 0;

    // 列表形式
    public TVChanneMenuIterator(List<MenuItem> menuItems) {
        this.menuItems = menuItems;
    }

    @Override
    public boolean hasNext() {
        if(position > menuItems.size()-1 || menuItems.get(position) == null){
            return false;
        }
        else{
            return true;
        }
    }

    @Override
    public Object next() {
        MenuItem menuItem = menuItems.get(position);
        position ++;
        return menuItem;
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 抽象聚合类:定义各聚合对象的数据增减,以及声明一个创建具体迭代器对象的方法,以用于对聚合对象进行遍历
/**
 * 抽象聚合类
 * 定义添加或删除聚合对象的方法
 * 并定义一个创建迭代器对象的方法
 */
public interface TelevisionMenu {

    // 添加聚合对象:频道,名称,介绍
    public void addItem(int channel ,String name, String description);

    // 创建迭代器对象
    public Iterator createIterator();


}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 具体聚合类:定义具体的聚合对象,该部分的存储结构可能不一致
/**
 * 具体聚合类
 * 电影界面,其也是主菜单的一部分
 * 其内容是一样的:频道,名称,介绍
 * 只是存储他们的结构不一样
 */
public class FilmMenu implements TelevisionMenu {

    static final int MAX_ITEMS = 5;    //菜单最大长度

    // 本聚合对象使用数组形式进行存储
    // 关联一个主菜单对象(总的上层对象)
    MenuItem[] menuItems;
    // 数组的最大长度
    int numberOfItems = 0;

    // 构造函数
    public FilmMenu() {
        menuItems = new MenuItem[MAX_ITEMS];

        addItem(1, "绝世天劫", "这是布鲁斯威利斯主演的...");
        addItem(2, "达芬奇密码", "这是我最喜欢的电影之一...");
        addItem(3, "黑客帝国123", "不知道你能够看懂不???");
        addItem(4, "我的女友是机器人", "一部我不反感的经典爱情电影...");
        addItem(5, "肖申克的救赎", "自由,幸福,离你有多远");
    }

    /**
     * @desc 将电影界面的item添加到主菜单项中,以实现统一的遍历
     * @param channel
     * @param name
     * @param description
     * @return void
     */
    @Override
    public void addItem(int channel, String name, String description) {
        MenuItem menuItem = new MenuItem(channel, name, description);

        if (numberOfItems > MAX_ITEMS) {
            System.out.println("不好意思,菜单满了....");
        }
        else {
            // 将新的电影菜单项加入主菜单
            menuItems[numberOfItems] = menuItem;
            numberOfItems++;
        }
    }

    // 创建该聚合对象的具体迭代器,以实现对本聚合对象的遍历(本聚合对象为数组形式)
    @Override
    public Iterator createIterator() {
        // 参数为本类中的聚合对象
        return new FilmMenuIterator(menuItems);
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
/**
 * 具体聚合类
 */
public class TVChanneMenu implements TelevisionMenu {

    // 本聚合对象使用List进行存储
    List<MenuItem> menuItems;

    public TVChanneMenu(){
        menuItems = new ArrayList<MenuItem>();
        addItem(1, "CCTV-1", "This is CCTV-1");
        addItem(2, "CCTV-2", "This is CCTV-2");
        addItem(3, "CCTV-3", "This is CCTV-3");
        addItem(4, "CCTV-4", "This is CCTV-4");
        addItem(5, "CCTV-5", "This is CCTV-5");
    }

    /**
     * @desc 将电视频道节目添加菜单集合中
     * @param channel  频道
     * @param name  名称
     * @param description  描述
     * @return void
     */
    @Override
    public void addItem(int channel, String name, String description) {
        MenuItem tvMenuItem = new MenuItem(channel, name, description);
        menuItems.add(tvMenuItem);
    }

    @Override
    public Iterator createIterator() {
        return new TVChanneMenuIterator(menuItems);
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 客户端
/**
 * 客户端
 */
public class Client {

    public static void main(String[] args) {
        TVChanneMenu tvMenu = new TVChanneMenu();
        FilmMenu filmMenu = new FilmMenu();

        MainMenu mainMenu = new MainMenu(tvMenu, filmMenu);
        mainMenu.printMenu();
    }

}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

15.4 特点

15.4.1 优点
  • 访问一个聚合对象的内容而无须暴露它的内部表示。

  • 遍历任务交由迭代器完成,这简化了聚合类。

  • 它支持以不同方式遍历一个聚合,甚至可以自定义迭代器的子类以支持新的遍历。

  • 增加新的聚合类和迭代器类都很方便,无须修改原有代码。

  • 封装性良好,为遍历不同的聚合结构提供一个统一的接口。

15.4.2 缺点

由于迭代器模式将存储数据和遍历数据的职责分离,增加新的聚合类需要对应增加新的迭代器类,类的个数成对增加,这在一定程度上增加了系统的复杂性。

15.5 总结

  • 迭代器模式提供一种方法来访问聚合对象,而不用暴露这个对象的内部表示。

  • 将遍历聚合对象中数据的行为提取出来,封装到一个迭代器中,通过专门的迭代器来遍历聚合对象的内部数据,这就是迭代器模式的本质。迭代器模式是“单一职责原则”的完美体现。

  • 当使用迭代器的时候,我们依赖聚合提供遍历。

  • 迭代器提供了一个通用的接口,让我们遍历聚合的项,放我们编码使用聚合项时,就可以使用多态机制。

十六、观察者模式

观察者(Observer)模式指多个对象间存在一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并被自动更新。这种模式有时又称作发布-订阅模式、模型-视图模式,它是对象行为型模式。

16.1 前言

在现实世界中,许多对象并不是独立存在的,其中一个对象的行为发生改变可能会导致一个或者多个其他对象的行为也发生改变。例如,某种商品的物价上涨时会导致部分商家高兴,而消费者伤心;还有,当我们开车到交叉路口时,遇到红灯会停,遇到绿灯会行。这样的例子还有很多,例如,股票价格与股民、微信公众号与微信用户、气象局的天气预报与听众、小偷与警察等。

在软件世界也是这样,例如,Excel 中的数据与折线图、饼状图、柱状图之间的关系;MVC 模式中的模型与视图的关系;事件模型中的事件源与事件处理者。所有这些,如果用观察者模式来实现就非常方便。

16.2 概述

一些面向对象的编程方式,提供了一种构建对象间复杂网络互连的能力。当对象们连接在一起时,它们就可以相互提供服务和信息。

通常来说,当某个对象的状态发生改变时,你仍然需要对象之间能互相通信。但是出于各种原因,你也许并不愿意因为代码环境的改变而对代码做大的修改。也许,你只想根据你的具体应用环境而改进通信代码。或者,你只想简单的重新构造通信代码来避免类和类之间的相互依赖与相互从属。

当一个对象的状态发生改变时,你如何通知其他对象?是否需要一个动态方案――一个就像允许脚本的执行一样,允许自由连接的方案?

观察者模式允许一个对象关注其他对象的状态,并且,观察者模式还为被观测者提供了一种观测结构,或者说是一个主体和一个客体。主体,也就是被观测者,可以用来联系所有的观测它的观测者。客体,也就是观测者,用来接受主体状态的改变 观测就是一个可被观测的类(也就是主题)与一个或多个观测它的类(也就是客体)的协作。不论什么时候,当被观测对象的状态变化时,所有注册过的观测者都会得到通知。
观察者模式将被观测者(主体)从观测者(客体)种分离出来。这样,每个观测者都可以根据主体的变化分别采取各自的操作。观察者模式和Publish/Subscribe模式一样,也是一种有效描述对象间相互作用的模式。观察者模式灵活而且功能强大。对于被观测者来说,那些查询哪些类需要自己的状态信息和每次使用那些状态信息的额外资源开销已经不存在了。另外,一个观测者可以在任何合适的时候进行注册和取消注册。你也可以定义多个具体的观测类,以便在实际应用中执行不同的操作。
将一个系统分割成一系列相互协作的类有一个常见的副作用:需要维护相关对象间的一致性。我们不希望为了维持一致性而使各类紧密耦合,因为这样降低了它们的可重用性。

16.2.1 类图

image-20200918090453552

  • **抽象主题角色(Subject):**也叫抽象目标类,该目标类知道它的观察者。可以有任意多个观察者观察同一个目标。它提供了一个用于保存观察者对象的聚集类和增加、删除观察者对象的方法,以及通知所有观察者的抽象方法。
  • **具体主题角色(ConcreteSubject):**也叫具体目标类,将有关状态存入各ConcreteObserver对象。它实现抽象目标中的通知方法,当具体主题的内部状态发生改变时,通知所有注册过的观察者对象。
  • **抽象观察者角色(Observer):**为那些在目标发生改变时需获得通知的对象定义一个更新接口。它是一个抽象类或接口,它包含了一个更新自己的抽象方法,当接到具体主题的更改通知时被调用。
  • **具体观察者角色(ConcreteObserver):**维护一个指向具体主题对象的引用。存储有关状态,这些状态应与目标的状态保持一致。实现抽象观察者中定义的抽象方法,以便在得到目标的更改通知时更新自身的状态。
16.2.2 适用场景
  • 对象间存在一对多关系,一个对象的状态发生改变会影响其他对象。

  • 当一个抽象模型有两个方面,其中一个方面依赖于另一方面时,可将这二者封装在独立的对象中以使它们可以各自独立地改变和复用。

  • 当一个对象必须通知其它对象,而它又不能假定其它对象是谁。换言之, 你不希望这些对象是紧密耦合的。

16.3 实现

在气象观测站中,它能够追踪目前的天气状况,包括温度、适度、气压。需要实现一个布告板,能够分别显示目前的状态,气象统计和简单的预报。当气象站中获取最新的测量数据时,三种布告板必须实时更新。

image-20200918093527400

/**
 * 外部类
 * 响铃事件
 * 一个外部类,将响铃事件封装为一个类
 */
public class RingEvent {

    private boolean sound;    //true表示上课铃声,false表示下课铃声
    public RingEvent(boolean sound)
    {
        this.sound=sound;
    }
    public void setSound(boolean sound)
    {
        this.sound=sound;
    }
    public boolean getSound()
    {
        return this.sound;
    }

}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
/**
 * 抽象主题类
 */
public interface Subject {

    // 增加观察者方法
    void add(Observer observer);

    // 删除观察者方法
    void remove(Observer observer);

    // 通知观察者方法
    void ring(boolean sound);
    // void notifyObserver(RingEvent event);

}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
/**
 * 具体主题类
 * 内部关联观察者对象
 * 它实现抽象目标中的通知方法,当具体主题的内部状态发生改变时,通知所有注册过的观察者对象。
 */
public class ConcreteSubject implements Subject {

    // 主题类知晓它的所有的观察者对象
    // 关联观察者对象,用于添加、删除与通知操作
    private List<Observer> observers = new ArrayList<>();

    public ConcreteSubject() {
    }

    // 添加观察者对象
    @Override
    public void add(Observer observer) {
        observers.add(observer);
    }

    // 删除观察者对象
    @Override
    public void remove(Observer observer) {
        observers.remove(observer);
    }

    // 事件触发器
    @Override
    public void ring(boolean sound)
    {
        String type=sound?"上课铃":"下课铃";
        System.out.println(type+"响!");

        // 初始化一个响铃事件对象
        RingEvent event=new RingEvent(sound);
        // 通知注册在该事件源上的所有观察者对象
        this.notifyObserver(event);
    }

    /**
     * 当事件发生时,通知绑定在该事件源上的所有监听器做出反应
     * @param event 引用外部类响铃事件
     */
    protected void notifyObserver(RingEvent event) {

        Observer observer;
        // 使用JAVA自带的迭代器,遍历所有的观察者
        Iterator<Observer> iterator = observers.iterator();
        while (iterator.hasNext()) {
            observer = iterator.next();
            // 调用观察者对象的响应方法
            observer.heardBell(event);
        }

    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
/**
 * 抽象观察者类
 * 监听响铃事件
 */
public interface Observer {

    //事件处理方法,听到铃声之后观察者的响应方法
    public void heardBell(RingEvent e);

}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
/**
 * 具体观察者
 */
public class Student implements Observer {
    @Override
    public void heardBell(RingEvent e) {
        if(e.getSound())
        {
            System.out.println("同学们,上课了...");
        }
        else
        {
            System.out.println("同学们,下课了...");
        }
    }
}

/**
 * 具体观察者
 */
public class Teacher implements Observer {
    @Override
    public void heardBell(RingEvent e) {
        if(e.getSound())
        {
            System.out.println("老师上课了...");
        }
        else
        {
            System.out.println("老师下课了...");
        }
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
/**
 * 客户端
 */
public class Client {

    public static void main(String[] args) {
        // 定义观察者
        Student student = new Student();
        Teacher teacher = new Teacher();

        // 具体的主题类
        ConcreteSubject concreteSubject = new ConcreteSubject();

        // 将观察者对象添加至主题类中
        concreteSubject.add(student);
        concreteSubject.add(teacher);

        // 响铃事件发生,将通知各个观察者
        concreteSubject.ring(true);
        System.out.println("------------------------------");
        concreteSubject.remove(student);
        concreteSubject.ring(false);

    }

}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26

16.4 特点

Observer模式允许你独立的改变目标和观察者。你可以单独复用目标对象而无需同时复用其观察者, 反之亦然。它也使你可以在不改动目标和其他的观察者的前提下增加观察者。

16.4.1 优点
  • 降低了目标与观察者之间的耦合关系,两者之间是抽象耦合关系。观察者模式提供了一种对象设计,让主题和观察者之间松耦合。主题所知道只是一个具体的观察者列表,每一个具体观察者都符合一个抽象观察者的接口。主题并不认识任何一个具体的观察者,它只知道他们都有一个共同的接口。

  • 目标与观察者之间建立了一套触发机制。观察者模式支持“广播通信”。主题会向所有的观察者发出通知。

  • 观察者模式符合“开闭原则”的要求。

16.4.2 缺点
  • 目标与观察者之间的依赖关系并没有完全解除,而且有可能出现循环引用。

  • 当观察者对象很多时,通知的发布会花费很多时间,影响程序的效率。

  • 观察者模式没有相应的机制让观察者知道所观察的目标对象是怎么发生变化的,而仅仅只是知道观察目标发生了变化。

16.5 总结

  1. 观察者模式定义了对象之间的一对多关系。多个观察者监听同一个被观察者,当该被观察者的状态发生改变时,会通知所有的观察者
  2. 观察者模式中包含四个角色。主题,它指被观察的对象。具体主题是主题子类,通常它包含有经常发生改变的数据,当它的状态发生改变时,向它的各个观察者发出通知;观察者,将对观察主题的改变做出反应;具体观察者中维护一个指向具体目标对象的引用,它存储具体观察者的有关状态,这些状态需要和具体目标的状态保持一致
  3. 主题用一个共同的接口来更新观察者
  4. 观察者与被观察者之间用松耦合方式结合
  5. 有多个观察者时,不可以依赖特定的通知次序
  6. 使用观察者模式,可以从被观察者处推或者拉数据。

十七、中介者模式

用一个中介对象来封装一系列的对象交互。中介者使各对象不需要显式地相互引用,从而使其耦合松散,而且可以独立地改变它们之间的交互。中介者模式又称为调停者模式。

17.1 前言

  • 在现实生活中,常常会出现好多对象之间存在复杂的交互关系,这种交互关系常常是“网状结构”,它要求每个对象都必须知道它需要交互的对象。例如,每个人必须记住他(她)所有朋友的电话;而且,朋友中如果有人的电话修改了,他(她)必须告诉其他所有的朋友修改,这叫作“牵一发而动全身”,非常复杂。如果把这种“网状结构”改为“星形结构”的话,将大大降低它们之间的“耦合性”,这时只要找一个“中介者”就可以了。如前面所说的“每个人必须记住所有朋友电话”的问题,只要在网上建立一个每个朋友都可以访问的“通信录”就解决了。这样的例子还有很多,例如,你刚刚参力口工作想租房,可以找“房屋中介”;或者,自己刚刚到一个陌生城市找工作,可以找“人才交流中心”帮忙。
  • 在软件的开发过程中,这样的例子也很多,例如,在 MVC 框架中,控制器(C)就是模型(M)和视图(V)的中介者;还有大家常用的 QQ 聊天程序的“中介者”是 QQ 服务器。所有这些,都可以采用“中介者模式”来实现,它将大大降低对象之间的耦合性,提高系统的灵活性。
  • 在地球上最大的中介者就是联合国了,它主要用来维护国际和平与安全、解决国际间经济、社会、文化和人道主义性质的问题。国与国之间的关系异常复杂,会因为各种各样的利益关系来结成盟友或者敌人,熟话说没有永远的朋友,也没有永远的敌人,只有永远的利益!所以国与国之间的关系同样会随着时间、环境因为利益而发生改变。在我们软件的世界也同样如此,对象与对象之间存在着很强、复杂的关联关系,如果没有类似于联合国这样的“机构”会很容易出问题的。

17.2 概述

在面向对象的软件设计与开发过程中,根据“单一职责原则”,我们应该尽量将对象细化,使其只负责或呈现单一的职责,即把行为分布到各个对象中。

对于一个模块或者系统,可能由很多对象构成,而且这些对象之间可能存在相互的引用,在最坏的情况下,每一个对象都知道其他所有的对象,这无疑复杂化了对象之间的联系。虽然将一个系统分割成许多对象通常可以增强可复用性,但是对象间相互连接的激增又会降低其可复用性,大量的相互连接使得一个对象似乎不太可能在没有其他对象的支持下工作,系统表现为一个不可分割的整体,而且对系统的行为进行任何较大的改动都会十分困难。会引发以下问题:

  • 对象与对象之间存在大量的关联关系,这样势必会导致系统的结构变得很复杂,同时若一个对象发生改变,我们也需要跟踪与之相关联的对象,同时做出相应的处理。

  • 对象之间的连接增加会导致对象可复用性降低。

  • 系统的可扩展性低。增加一个新的对象,我们需要在其相关连的对象上面加上引用,这样就会导致系统的耦合性增高,使系统的灵活性和可扩展都降低。

如果两个类不必彼此通信,那么这两个类就不应当发生直接关联的关系。如果其中一个类需要调用另一个类中的方法,我们可以通过第三方来转发这个调用。所以对于关系比较复杂的系统,我们为了减少对象之间的关联关系,使之成为一个松耦合系统,我们就需要使用中介者模式。

通过中介者模式,我们可以将复杂关系的网状结构变成结构简单的以中介者为核心的星形结构,每个对象不再和它与之关联的对象直接发生相互作用,而是通过中介者对象来另一个对象发生相互作用。

17.2.1 类图

所谓中介者模式就是用一个中介对象来封装一系列的对象交互,中介者使各对象不需要显式地相互引用,从而使其耦合松散,而且可以独立地改变它们之间的交互。

通过定义我们可以看出中介者主要是通过中介对象来封装对象之间的关系,使之各个对象在不需要知道其他对象的具体信息情况下通过中介者对象来与之通信。同时通过引用中介者对象来减少系统对象之间关系,提高了对象的可复用和系统的可扩展性。

但是就是因为中介者对象封装了对象之间的关联关系,导致中介者对象变得比较庞大,所承担的责任也比较多。它需要知道每个对象和他们之间的交互细节,如果它出问题,将会导致整个系统都会出问题。**所以它比较容易应用也很容易误用。**故当系统中出现了“多对多”交互复杂的关系群时,千万别急着使用中介者模式,你首先需要做的就是反思你的系统在设计上是不是合理。

image-20200919093653475

  • **抽象中介者(Mediator):**它是中介者的接口,提供了同事对象注册与转发同事对象信息的抽象方法
  • **具体中介者(ConcreteMediator):**具体中介者通过协调各同事对象实现协作行为。了解并维护它的各个同事。实现中介者接口,定义一个 List 来管理同事对象,协调各个同事角色之间的交互关系
  • **抽象同事类(Colleague):**定义同事类接口,定义各同事的公有方法。保存中介者对象,提供同事对象交互的抽象方法,实现所有相互影响的同事类的公共功能
  • **具体同事类(ConcreteColleague):**实现抽象同事类中的方法。每一个同事类需要知道中介者对象;每个具体同事类只需要了解自己的行为,而不需要了解其他同事类的情况。每一个同事对象在需与其他的同事通信的时候,与它的中介者通信
17.2.2 适用场景
  • 系统中对象之间存在比较复杂的引用关系,导致他们之间的依赖关系结构混乱而且难以复用该对象

  • 想通过一个中间类来封装多个类中的行为,而又不想生成太多的子类

  • 一组对象以定义良好但复杂的方式进行通信。产生的相互依赖关系结构混乱且难以理解

  • 一个对象引用其他很多对象并且直接与这些对象通信 ,导致难以复用该对象。

17.3 实现

用中介者模式编写一个“韶关房地产交流平台”程序。韶关房地产交流平台是“房地产中介公司”提供给“卖方客户”与“买方客户”进行信息交流的平台,比较适合用中介者模式来实现。

首先,定义一个中介公司接口,它是抽象中介者,它包含了客户注册方法 register和信息转发方法 relay;再定义一个韶关房地产中介公司,它是具体中介者类,它包含了保存客户信息的 List 对象,并实现了中介公司中的抽象方法。

然后,定义一个客户类,它是抽象同事类,其中包含了中介者的对象,和发送信息的 send方法与接收信息的 receive方法的接口。

最后,定义卖方(Seller)类和买方(Buyer)类,它们是具体同事类,是客户(Customer)类的子类,它们实现了父类中的抽象方法,通过中介者类进行信息交流。

/**
 * 抽象中介者
 * 提供了同事对象注册与转发同事对象信息的抽象方法
 */
public interface Mediator {
    // 客户注册
    void register(Customer member);

    // 转发
    void relay(String from, String ad);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
/**
 * 具体中介者:中介公司
 * 实现中介者接口
 * 中介者必须要知道同事类的信息:定义一个List来管理同事对象,协调各个同事角色之间的交互关系
 */
public class MediatorCompany implements Mediator {

    // 定义一个List来管理同事类(客户)
    private List<Customer> members = new ArrayList<>();

    // 首先要将同事类对象注册至中介者中,才可进行后续的操作
    @Override
    public void register(Customer member) {
        if (!members.contains(member)) {
            members.add(member);
            // 将本中介者与当前的会员进行绑定
            member.setMedium(this);
        }
    }

    /**
     * 中介者将信息进行转发
     *
     * @param from 信息源
     * @param ad   信息内容
     */
    @Override
    public void relay(String from, String ad) {
        for (Customer ob : members) {
            String name = ob.getName();
            // 将这些信息发送至与该中介者关联的除发送方外的其他所有同事类对象
            if (!name.equals(from)) {
                // 其他同事类对象接收信息
                ob.receive(from, ad);
            }
        }
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
/**
 * 抽象同事类
 * 定义各同事的公有方法
 * 保存中介者对象
 */
public abstract class Customer {

    // 中介者对象
    private Mediator medium;
    // 顾客的姓名
    private String name;

    public Customer(String name) {
        this.name = name;
    }

    // 设置该同事类关联的中介者对象
    public void setMedium(Mediator medium) {
        this.medium = medium;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Mediator getMedium() {
        return medium;
    }

    public String getName() {
        return name;
    }

    // 定义同事类的公共方法
    //1 发送信息
    public abstract void send(String ad);

    /**
     * 2 接收信息
     *
     * @param from 信息源
     * @param ad   信息内容
     */
    public abstract void receive(String from, String ad);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
/**
 * 具体同事类:卖方
 * 实现抽象同事类中的方法
 * 每一个同事类需要知道中介者对象
 */
public class Seller extends Customer {

    public Seller(String name) {
        super(name);
    }

    @Override
    public void send(String ad) {
        System.out.println("卖方" + super.getName() + "说:" + ad);

        // 通过中介者将卖方的信息发送出去
        super.getMedium().relay(super.getName(), ad);
    }

    @Override
    public void receive(String from, String ad) {
        System.out.println(super.getName()+"接收到来自" + from + "的消息:" + ad);
    }
}

/**
 * 具体同事类:买方
 * 实现抽象同事类中的方法
 * 每一个同事类需要知道中介者对象
 */
public class Buyer extends Customer {

    public Buyer(String name) {
        super(name);
    }

    @Override
    public void send(String ad) {
        System.out.println("买房" + super.getName() + "说:" + ad);

        // 通过中介者将买方的信息发送出去
        super.getMedium().relay(super.getName(), ad);
    }

    @Override
    public void receive(String from, String ad) {
        System.out.println(super.getName()+"接收到来自" + from + "的消息:" + ad);
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
/**
 * 客户端
 * 内部含有具体同事类对象
 * 主要操作中介者对象,将同事类对象注册至中介者中
 */
public class Client {

    public static void main(String[] args) {
        // 定义一个中介者对象
        Mediator mediator = new MediatorCompany();

        Customer seller = new Seller("张三");
        Customer buyer1 = new Buyer("李四");
        Customer buyer2 = new Buyer("王五");

        // 将具体同事类注册至中介者中进行管理
        mediator.register(seller);
        mediator.register(buyer1);
        mediator.register(buyer2);

        // 双方只需负责发送消息即可,其具体的交互传递细节交由中介者完成
        seller.send("我要卖房,好几套.....");

        buyer1.send("我要买1套.....");
        buyer2.send("我要买5套.....");

    }

}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29

17.4 特点

17.4.1 优点
  • 降低了对象之间的耦合性,使得对象易于独立地被复用。

  • 将对象间的一对多关联转变为一对一的关联,提高系统的灵活性,使得系统易于维护和扩展。

  • 简化了对象之间的关系,将系统的各个对象之间的相互关系进行封装,将各个同事类解耦,使系统成为松耦合系统。

  • 减少了子类生成。Mediator将原本分布于多个对象间的行为集中在一起。改变这些行为只需生成Mediator的子类即可。这样各个Colleague类可被重用。

  • 它使控制集中化。中介者模式将交互的复杂性变为中介者的复杂性。

17.4.2 缺点

由于中介者对象封装了系统中对象之间的相互关系,导致其变得非常复杂,使得系统维护比较困难。在具体中介者类中包含了同事之间的交互细节,可能会导致具体中介者类非常复杂,这可能使得中介者自身成为一个难于维护的
庞然大物。

17.5 总结

在面向对象编程中,一个类必然会与其他的类发生依赖关系,完全独立的类是没有意义的。一个类同时依赖多个类的情况也相当普遍,既然存在这样的情况,说明,一对多的依赖关系有它的合理性,适当的使用中介者模式可以使原本凌乱的对象关系清晰,但是如果滥用,则可能会带来反的效果。一般来说,只有对于那种同事类之间是网状结构的关系,才会考虑使用中介者模式。可以将网状结构变为星状结构,使同事类之间的关系变的清晰一些。

  1. 在中介者模式中通过引用中介者对象,将系统中有关的对象所引用的其他对象数目减少到最少。它简化了系统的结构,将系统由负责的网状结构转变成简单的星形结构,中介者对象在这里起到中转和协调作用。

  2. 中介者类是中介者模式的核心,它对整个系统进行控制和协调,简化了对象之间的交互,还可以对对象间的交互进行进一步的控制。

  3. 通过使用中介者模式,具体的同事类可以独立变化,通过引用中介者可以简化同事类的设计和实现。

  4. 就是由于中介者对象需要知道所有的具体同事类,封装具体同事类之间相互关系,导致中介者对象变得非常复杂,系统维护起来较为困难。

十八、备忘录模式

在不破坏封装性的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态,以便以后当需要时能将该对象恢复到原先保存的状态。该模式又叫快照模式。

18.1 前言

每个人都有犯错误的时候,都希望有种“后悔药”能弥补自己的过失,让自己重新开始,但现实是残酷的。在计算机应用中,客户同样会常常犯错误,能否提供“后悔药”给他们呢?当然是可以的,而且是有必要的。这个功能由“备忘录模式”来实现。

其实很多应用软件都提供了这项功能,如 Word、记事本、Photoshop、Eclipse 等软件在编辑时按 Ctrl+Z 组合键时能撤销当前操作,使文档恢复到之前的状态;还有在 IE 中的后退键、数据库事务管理中的回滚操作、玩游戏时的中间结果存档功能、数据库与操作系统的备份操作、棋类游戏中的悔棋功能等都属于这类。

备忘录模式能记录一个对象的内部状态,当用户后悔时能撤销当前操作,使数据恢复到它原先的状态。

18.2 概述

备忘录模式是一种给我们的软件提供后悔药的机制,通过它可以使系统恢复到某一特定的历史状态。

18.2.1 类图
image-20200921104014830
  • **Originator原发器:**记录当前时刻的内部状态信息,提供创建备忘录和恢复备忘录数据的功能。负责创建一个备忘录,用以记录当前对象的内部状态,也可以使用它来利用备忘录恢复内部状态。同时原发器还可以根据需要决定Memento存储Originator的哪些内部状态。

  • **Memento备忘录:**用于存储Originator的内部状态,负责存储发起人的内部状态,在需要的时候提供这些内部状态给发起人。在备忘录Memento中有两个接口,负责设置和获取状态。其中Caretaker只能看到备忘录中的窄接口,它只能将备忘录传递给其他对象。Originator可以看到宽接口,允许它访问返回到先前状态的所有数据。

  • **Caretaker负责人:**对备忘录进行管理,提供保存与获取备忘录的功能,但其不能对备忘录的内容进行访问与修改。负责触发发起者的变化或触发发起者返回先前状态的动作的类。

在备忘录模式中,最重要的就是备忘录Memento了。备忘录中存储的就是原发器的部分或者所有的状态信息,而这些状态信息是不能够被其他对象所访问的,也就是说我们是不可能在备忘录之外的对象来存储这些状态信息,如果暴露了内部状态信息就违反了封装的原则,所以备忘录是除了原发器外其他对象都不可以访问的。所以为了实现备忘录模式的封装,我们需要对备忘录的访问做些控制:

对原发器:可以访问备忘录里的所有信息。

对负责人:不可以访问备忘录里面的数据,但是他可以保存备忘录并且可以将备忘录传递给其他对象。

其他对象:不可访问也不可以保存,它只负责接收从负责人那里传递过来的备忘录同时恢复原发器的状态。

所以就备忘录模式而言理想的情况就是只允许生成该备忘录的那个原发器访问备忘录的内部状态。

18.2.2 适用场景
  • 需要保存一个对象在某一个时刻的状态或部分状态。

  • 如果用一个接口来让其他对象得到这些状态,将会暴露对象的实现细节并破坏对象的封装性,一个对象不希望外界直接访问其内部状态,通过负责人可以间接访问其内部状态。

  • 需要提供一个可回滚操作的场景,如 Word、记事本、Photoshop,Eclipse 等软件在编辑时按 Ctrl+Z 组合键,还有数据库中事务操作。

18.3 实现

以游戏挑战BOSS为实现场景,在挑战BOSS之前,角色的血量、蓝量都是满值,然后存档,在大战BOSS时,由于操作失误导致血量和蓝量大量损耗,所以只好恢复到刚刚开始的存档点,继续进行大战BOSS了。

/**
 * 发起者类
 * 发起者类是我们需要记住状态的对象,以便在某个时刻恢复它
 */
public class Role {

    // 记录血量、蓝量属性值
    private int bloodFlow;
    private int magicPoint;

    public Role(int bloodFlow, int magicPoint) {
        this.bloodFlow = bloodFlow;
        this.magicPoint = magicPoint;
    }

    public int getBloodFlow() {
        return bloodFlow;
    }

    public void setBloodFlow(int bloodFlow) {
        this.bloodFlow = bloodFlow;
    }

    public int getMagicPoint() {
        return magicPoint;
    }

    public void setMagicPoint(int magicPoint) {
        this.magicPoint = magicPoint;
    }

    /**
     * @return void
     * @desc 展示角色当前状态
     */
    public void display() {
        System.out.println("用户当前状态:");
        System.out.println("血量:" + getBloodFlow() + ";蓝量:" + getMagicPoint());
    }

    /**
     * 存档:将当前的状态信息保存至备忘录中
     * @return Memento
     * @desc 存档,保存当前状态
     */
    public Memento saveMemento() {
        return new Memento(getBloodFlow(), getMagicPoint());
    }

    /**
     * 借助管理者,获得之前保存的备忘录对象,然后获取其原来的状态信息
     * @param memento
     * @return void
     * @desc 恢复存档
     */
    public void restoreMemento(Memento memento) {
        this.bloodFlow = memento.getBloodFlow();
        this.magicPoint = memento.getMagicPoint();
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
/**
 * 备忘录类
 * 负责存储发起者的内部状态
 */
public class Memento {

    // 备忘录拥有与发起者类相同的属性
    private int bloodFlow;
    private int magicPoint;

    public Memento(int bloodFlow, int magicPoint) {
        this.bloodFlow = bloodFlow;
        this.magicPoint = magicPoint;
    }

    public int getBloodFlow() {
        return bloodFlow;
    }

    public void setBloodFlow(int bloodFlow) {
        this.bloodFlow = bloodFlow;
    }

    public int getMagicPoint() {
        return magicPoint;
    }

    public void setMagicPoint(int magicPoint) {
        this.magicPoint = magicPoint;
    }

}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
/**
 * 管理者
 * 管理者类是实现状态恢复的核心类
 * 对备忘录进行管理,提供保存与获取备忘录的功能,但其不能对备忘录的内容进行访问与修改。
 */
public class Caretaker {

    // 聚合一个备忘录对象,用以管理备忘录对象
    private Memento memento;

    public Memento getMemento() {
        return memento;
    }

    public void setMemento(Memento memento) {
        this.memento = memento;
    }

}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
/**
 * 客户端
 */
public class Client {

    public static void main(String[] args) {
        // 打BOSS之前:血、蓝全部满值
        Role role = new Role(100, 100);
        System.out.println("----------大战BOSS之前----------");
        role.display();

        // 保持进度
        Caretaker caretaker = new Caretaker();
        // 将最新的状态保存至备忘录,并将该备忘录交由管理者进行管理
        caretaker.setMemento(role.saveMemento());

        // 大战BOSS,状态值减少
        role.setBloodFlow(20);
        role.setMagicPoint(20);
        System.out.println("----------大战BOSS----------");
        role.display();

        // 恢复存档
        role.restoreMemento(caretaker.getMemento());
        System.out.println("----------恢复----------");
        role.display();
    }

}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29

18.4 特点

18.4.1 优点
  • 提供了一种可以恢复状态的机制。当用户需要时能够比较方便地将数据恢复到某个历史的状态。
  • 实现了内部状态的封装。除了创建它的发起人之外,其他对象都不能够访问这些状态信息。
  • 简化了发起人类。发起人不需要管理和保存其内部状态的各个备份,所有状态信息都保存在备忘录中,并由管理者进行管理,这符合单一职责原则。
18.4.2 缺点

资源消耗大。如果要保存的内部状态信息过多或者特别频繁,将会占用比较大的内存资源。如果类的成员变量过多,势必会占用比较大的资源,而且每一次保存都会消耗一定的内存。

18.5 总结

  1. 备忘录模式可以实现在不破坏封装的前提下,捕获一个类的内部状态,并且在该对象之外保存该对象的状态,保证该对象能够恢复到历史的某个状态。
  2. 备忘录模式实现了内部状态的封装,除了创建它的原发器之外其他对象都不能够访问它。
  3. 备忘录模式会占用较多的内存,消耗资源。

十九、状态模式

对有状态的对象,把复杂的“判断逻辑”提取到不同的状态对象中,允许状态对象在其内部状态发生改变时改变其行为。

19.1 背景

在软件开发过程中,应用程序中的有些对象可能会根据不同的情况做出不同的行为,我们把这种对象称为有状态的对象,而把影响对象行为的一个或多个动态变化的属性称为状态。当有状态的对象与外部事件产生互动时,其内部状态会发生改变,从而使得其行为也随之发生改变。如人的情绪有高兴的时候和伤心的时候,不同的情绪有不同的行为,当然外界也会影响其情绪变化。

比如你要预订房间,那么只有当该房间为空闲时你才能预订,你想入住该房间也只有当你预订了该房间或者该房间为空闲时。对于这样的一个对象,当它在于外部事件产生互动的时候,其内部状态就会发生改变,从而使得他的行为也随之发生改变。

对这种有状态的对象编程,传统的解决方案是:将这些所有可能发生的情况全都考虑到,然后使用 if-else 语句来做状态判断,再进行不同情况的处理。但当对象的状态很多时,程序会变得很复杂。而且增加新的状态要添加新的 if-else 语句,这违背了“开闭原则”,不利于程序的扩展。

以上问题如果采用“状态模式”就能很好地得到解决。状态模式的解决思想是:当控制一个对象状态转换的条件表达式过于复杂时,把相关“判断逻辑”提取出来,放到一系列的状态类当中,这样可以把原来复杂的逻辑判断简单化。

19.2 概述

状态模式把受环境改变的对象行为包装在不同的状态对象里,其意图是让一个对象在其内部状态改变的时候,其行为也随之改变。

19.2.1 类图

image-20200922101819631

  • 环境类(Context): 定义客户感兴趣的接口。维护一个ConcreteState子类的实例,即当前的状态,并将与状态相关的操作委托给当前状态对象来处理。
  • 抽象状态类(State): 定义一个接口,用以封装与环境对象Context的特定状态所相关的行为。
  • 具体状态类(ConcreteState): 用来处理来自Context的请求,每一个具体状态类都提供了它对自己请求的实现,所以当Context改变状态时行为也会跟着改变。
19.2.2 适用场景
  • 当一个对象的行为取决于它的状态,并且它必须在运行时根据状态改变它的行为时,就可以考虑使用状态模式。
  • 代码中包含大量与对象状态有关的条件语句。一个操作中含有庞大的分支结构,并且这些分支决定于对象的状态时。

19.3 实现

用“状态模式”设计一个多线程的状态转换程序。多线程存在 5 种状态,分别为新建状态、就绪状态、运行状态、阻塞状态和死亡状态,各个状态当遇到相关方法调用或事件触发时会转换到其他状态:

image-20200922102814076

先定义一个抽象状态类(TheadState),然后为每个状态设计一个具体状态类,它们是新建状态(New)、就绪状态(Runnable )、运行状态(Running)、阻塞状态(Blocked)和死亡状态(Dead)。每个状态中有触发它们转变状态的方法,环境类(ThreadContext)中先生成一个初始状态(New),并提供相关触发方法。

image-20200922102919397

/**
 * 抽象状态类
 * 封装与环境对象Context的特定状态所相关的行为
 */
public abstract class ThreadState {
    //状态名
    protected String stateName;

    // 启动
    public abstract void start(ThreadContext hj);

    // 就绪
    public abstract void getCPU(ThreadContext hj);

    // 暂停
    public abstract void suspend(ThreadContext hj);

    // 停止
    public abstract void stop(ThreadContext hj);

    // 重启
    public abstract void resume(ThreadContext hj);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
/**
 * 具体状态类:新建状态
 * 新建状态可以转变为就绪状态
 */
public class New extends ThreadState {

    public New() {
        stateName = "新建状态";
        System.out.println("当前线程处于:新建状态.");
    }

    // 调用start方法,使进程进入就绪状态
    @Override
    public void start(ThreadContext hj) {
        System.out.print("调用start()方法-->");
        if(stateName.equals("新建状态"))
        {
            hj.setState(new Runnable());
        }
        else
        {
            System.out.println("当前线程不是新建状态,不能调用start()方法.");
        }
    }

    @Override
    public void getCPU(ThreadContext hj) {

    }

    @Override
    public void suspend(ThreadContext hj) {

    }

    @Override
    public void stop(ThreadContext hj) {

    }

    @Override
    public void resume(ThreadContext hj) {

    }
}

/**
 * 具体状态类:就绪状态
 */
public class Runnable extends ThreadState {

    public Runnable() {
        stateName = "就绪状态";
        System.out.println("当前线程处于:就绪状态.");
    }

    @Override
    public void start(ThreadContext hj) {

    }

    // 获取CPU时间,使进程进入运行状态
    @Override
    public void getCPU(ThreadContext hj) {
        System.out.print("获得CPU时间-->");
        if (stateName.equals("就绪状态")) {
            hj.setState(new Running());
        } else {
            System.out.println("当前线程不是就绪状态,不能获取CPU.");
        }
    }

    @Override
    public void suspend(ThreadContext hj) {

    }

    @Override
    public void stop(ThreadContext hj) {

    }

    @Override
    public void resume(ThreadContext hj) {

    }
}


/**
 * 具体状态类:运行状态
 * 运行状态之后可以进入阻塞与死亡状态
 */
public class Running extends ThreadState {

    public Running() {
        stateName = "运行状态";
        System.out.println("当前线程处于:运行状态.");
    }

    @Override
    public void start(ThreadContext hj) {

    }

    @Override
    public void getCPU(ThreadContext hj) {

    }

    // 运行状态下,可以执行挂起操作,将其转变为阻塞状态
    // 由运行状态变为阻塞状态
    @Override
    public void suspend(ThreadContext hj) {
        System.out.print("调用suspend()方法-->");
        if (stateName.equals("运行状态")) {
            hj.setState(new Blocked());
        } else {
            System.out.println("当前线程不是运行状态,不能调用suspend()方法.");
        }
    }

    // 还可以执行kill操作,杀死进程
    // 由运行状态变为死亡状态
    @Override
    public void stop(ThreadContext hj) {
        System.out.print("调用stop()方法-->");
        if (stateName.equals("运行状态")) {
            hj.setState(new Dead());
        } else {
            System.out.println("当前线程不是运行状态,不能调用stop()方法.");
        }
    }

    @Override
    public void resume(ThreadContext hj) {

    }
}

/**
 * 具体状态类:阻塞状态
 */
public class Blocked extends ThreadState {

    public Blocked()
    {
        stateName="阻塞状态";
        System.out.println("当前线程处于:阻塞状态.");
    }

    @Override
    public void start(ThreadContext hj) {

    }

    @Override
    public void getCPU(ThreadContext hj) {

    }

    @Override
    public void suspend(ThreadContext hj) {

    }

    @Override
    public void stop(ThreadContext hj) {

    }

    // 阻塞状态下只能调用重新唤醒操作,重新进入就绪状态
    @Override
    public void resume(ThreadContext hj) {
        System.out.print("调用resume()方法-->");
        if(stateName.equals("阻塞状态"))
        {
            hj.setState(new Runnable());
        }
        else
        {
            System.out.println("当前线程不是阻塞状态,不能调用resume()方法.");
        }
    }
}

/**
 * 具体状态类:死亡状态
 * 死亡状态下,不进行任何操作
 */
public class Dead extends ThreadState {

    public Dead() {
        stateName="死亡状态";
        System.out.println("当前线程处于:死亡状态.");
    }

    @Override
    public void start(ThreadContext hj) {

    }

    @Override
    public void getCPU(ThreadContext hj) {

    }

    @Override
    public void suspend(ThreadContext hj) {

    }

    @Override
    public void stop(ThreadContext hj) {

    }

    @Override
    public void resume(ThreadContext hj) {

    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108
  • 109
  • 110
  • 111
  • 112
  • 113
  • 114
  • 115
  • 116
  • 117
  • 118
  • 119
  • 120
  • 121
  • 122
  • 123
  • 124
  • 125
  • 126
  • 127
  • 128
  • 129
  • 130
  • 131
  • 132
  • 133
  • 134
  • 135
  • 136
  • 137
  • 138
  • 139
  • 140
  • 141
  • 142
  • 143
  • 144
  • 145
  • 146
  • 147
  • 148
  • 149
  • 150
  • 151
  • 152
  • 153
  • 154
  • 155
  • 156
  • 157
  • 158
  • 159
  • 160
  • 161
  • 162
  • 163
  • 164
  • 165
  • 166
  • 167
  • 168
  • 169
  • 170
  • 171
  • 172
  • 173
  • 174
  • 175
  • 176
  • 177
  • 178
  • 179
  • 180
  • 181
  • 182
  • 183
  • 184
  • 185
  • 186
  • 187
  • 188
  • 189
  • 190
  • 191
  • 192
  • 193
  • 194
  • 195
  • 196
  • 197
  • 198
  • 199
  • 200
  • 201
  • 202
  • 203
  • 204
  • 205
  • 206
  • 207
  • 208
  • 209
  • 210
  • 211
  • 212
  • 213
  • 214
  • 215
  • 216
  • 217
  • 218
  • 219
  • 220
  • 221
  • 222
/**
 * 环境类
 * 是客户端更改状态的接口
 */
public class ThreadContext {

    // 聚合一个抽象的状态对象,用以控制对状态的更改操作
    private ThreadState state;

    // 初始化时,默认将其设为新建状态
    public ThreadContext() {
        this.state = new New();
    }

    public ThreadState getState() {
        return state;
    }

    public void setState(ThreadState state) {
        this.state = state;
    }

    public void start()
    {
        state.start(this);
    }
    public void getCPU()
    {
        state.getCPU(this);
    }
    public void suspend()
    {
        state.suspend(this);
    }
    public void stop()
    {
        state.stop(this);
    }
    public void resume()
    {
        state.resume(this);
    }

}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
/**
 * 客户端类
 */
public class Client {
    public static void main(String[] args)
    {
        // 客户端操作的是ThreadContext
        ThreadContext context=new ThreadContext();
        context.start();    // 新建——>就绪
        context.getCPU();   // 就绪——>运行
        context.suspend();  // 运行——>阻塞
        context.resume();   // 阻塞——>就绪
        context.getCPU();   // 就绪——>运行
        context.stop();     // 运行——>死亡
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

19.4 特点

19.4.1 优点
  • 状态模式将与特定状态相关的行为局部化到一个状态中,并且将不同状态的行为分割开来,满足“单一职责原则”。
  • 减少对象间的相互依赖。将不同的状态引入独立的对象中会使得状态转换变得更加明确,且减少对象间的相互依赖。
  • 有利于程序的扩展。通过定义新的子类很容易地增加新的状态和转换。
19.4.2 缺点
  • 状态模式的使用必然会增加系统类和对象的个数
  • 状态模式的结构与实现都较为复杂,如果使用不当将导致程序结构和代码的混乱
  • 状态模式对“开闭原则”的支持并不太好,对于可以切换状态的状态模式,增加新的状态类需要修改那些负责状态转换的源代码,否则无法切换到新增状态;而且修改某个状态类的行为也需修改对应类的源代码

19.5 相关模式

19.5.1 职责链模式

职责链模式和状态模式都可以解决If分支语句过多。
从定义来看,状态模式是一个对象的内在状态发生改变(一个对象,相对比较稳定,处理完一个对象下一个对象的处理一般都已确定),而职责链模式是多个对象之间的改变(多个对象之间的话,就会出现某个对象不存在的现在,就像我们举例的公司请假流程,经理可能不在公司情况),这也说明他们两个模式处理的情况不同。

这两个设计模式最大的区别就是状态模式是让各个状态对象自己知道其下一个处理的对象是谁。而职责链模式中的各个对象并不指定其下一个处理的对象到底是谁,只有在客户端才设定。

  • 状态模式相当于If else if else:

    设计路线:各个State类的内部实现(相当于If,else If内的条件)执行时通过State调用Context方法来执行。

  • 职责链模式相当于Switch case:

    设计路线:由客户设定每个子类的参数是哪一个一个子类。使用时,向链的第一个子类的执行方法传递参数就可以。

就像对设计模式的总结,有的人采用的是状态模式,从头到尾,提前一定定义好下一个处理的对象是谁,而我采用的是职责链模式,随时都有可能调整链的顺序。即状态模式的处理顺序是固定的,而职责链模式则是由客户端来根据需求进行设置的。

19.5.2 策略模式

状态模式是策略模式的孪生兄弟。状态模式和策略模式的实现方法非常类似,都是利用多态把一些操作分配到一组相关的简单的类中,因此很多人认为这两种模式实际上是相同的。

然而在现实世界中,策略和状态是两种完全不同的思想。当我们对状态和策略进行建模时,这种差异会导致完全不同的问题。例如,对状态进行建模时,状态迁移是一个核心内容;然而,在选择策略时,迁移与此毫无关系。另外,策略模式允许一个客户选择或提供一种策略,而这种思想在状态模式中完全没有。

一个策略是一个计划或方案,通过执行这个计划或方案,我们可以在给定的输入条件下达到一个特定的目标。策略是一组方案,他们可以相互替换;选择一个策略,获得策略的输出。策略模式用于随不同外部环境采取不同行为的场合。而状态模式不同,对一个状态特别重要的对象,通过状态机来建模一个对象的状态;状态模式处理的核心问题是状态的迁移,因为在对象存在很多状态情况下,对各个工作流,各个状态之间跳转和迁移过程都是及其复杂的。

例如一个工作流,审批一个文件,存在新建、提交、已修改、HR部门审批中、老板审批中、HR审批失败、老板审批失败等状态,涉及多个角色交互,涉及很多事件,这种情况下用状态模式(状态机)来建模更加合适;把各个状态和相应的实现步骤封装成一组简单的继承自一个接口或抽象类的类,通过另外的一个Context来操作他们之间的自动状态变换,通过event来自动实现各个状态之间的跳转。在整个生命周期中存在一个状态的迁移曲线,这个迁移曲线对客户是透明的。

在状态模式中,状态的变迁是由对象的内部条件决定,外界只需关心其接口,不必关心其状态对象的创建和转化;
而策略模式里,采取何种策略由外部条件©决定。

他们应用场景却不一样,State模式重在强调对象内部状态的变化改变对象的行为,策略模式重在外部对策略的选择,策略的选择由外部条件决定,也就是说算法的动态的切换。但由于它们的结构是如此的相似,我们可以认为“状态模式是完全封装且自修改的策略模式”。即状态模式是封装对象内部的状态的,而策略模式是封装算法族的。

19.6 总结

状态模式的主要优点在于封装了转换规则,并枚举可能的状态,它将所有与某个状态有关的行为放到一个类中,并且可以方便地增加新的状态,只需要改变对象状态即可改变对象的行为,还可以让多个环境对象共享一个状态对象,从而减少系统中对象的个数;其缺点在于使用状态模式会增加系统类和对象的个数,且状态模式的结构与实现都较为复杂,如果使用不当将导致程序结构和代码的混乱,对于可以切换状态的状态模式不满足“开闭原则”的要求。

二十、策略模式

策略模式定义了一系列算法,并将每个算法封装起来,使它们可以相互替换,且算法的变化不会影响使用算法的客户。策略模式属于对象行为模式,它通过对算法进行封装,把使用算法的责任和算法的实现分割开来,并委派给不同的对象对这些算法进行管理。

20.1 背景

首先我们需要知道策略模式与状态模式是如此的相似,就犹如一对双胞胎一样。只不过状态模式是通过改变对象内部的状态来帮助对象控制自己的行为,而策略模式则是围绕可以互换的算法来创建成功业务的。两者都可用于解决同一个问题:带有大量的if…else…等条件判断语句来进行选择的。

在现实生活中常常遇到实现某种目标存在多种策略可供选择的情况,例如,出行旅游可以乘坐飞机、乘坐火车、骑自行车或自己开私家车等,超市促销可以釆用打折、送商品、送积分等方法。

在软件开发中也常常遇到类似的情况,当实现某一个功能存在多种算法或者策略,我们可以根据环境或者条件的不同选择不同的算法或者策略来完成该功能,如数据排序策略有冒泡排序、选择排序、插入排序、二叉树排序等。

如果使用多重条件转移语句实现(即硬编码),不但使条件语句变得很复杂,而且增加、删除或更换算法要修改原代码,不易维护,违背开闭原则。如果采用策略模式就能很好解决该问题。

20.2 概述

策略模式把对象本身和运算规则区分开来,其功能非常强大,因为这个设计模式本身的核心思想就是面向对象编程的多态的思想。

在软件系统中有很多种方法可以实现同一个功能,比如排序算法它有冒泡排序、选择排序、快速排序、插入排序等等。这里我们有一种硬编码方法,就是讲所以的排序算法全部写在一个类中,每一种算法的具体实现对应着一个方法,然后写一个总方法通过if…else…来判断选择具体的排序算法,但是这样做存在几个问题:

  • 如果需要增加新的算法,则需要修改源代码。

  • 如果更新了排序算法,那么需要在客户端也需要修改代码,麻烦。

  • 充斥着大量的if…else…语句,代码维护比较困难。

所以为了解决这些问题,我们可以定义一些独立的类来封装不同的算法,每一个独立的类对应着一个具体的算法实现,在这里我们就将这里每一个独立的类称之为一个策略。

20.2.1 类图

image-20200923103353275

  • **环境类(Context):**用一个ConcreteStrategy对象来配置。维护一个对Strategy对象的引用。可定义一个接口来让Strategy访问它的数据。最终被客户端调用。
  • **抽象策略类(Strategy):**定义所有支持的算法的公共接口。 Context使用这个接口来调用某ConcreteStrategy定义的算法。
  • **具体策略类(ConcreteStrategy):**以Strategy接口实现某具体算法。

策略模式的UML图与状态模式的几乎一模一样,状态模式是通过改变对象内部的状态来帮助对象控制自己的行为,状态模式其实是对状态进行封装,它是将动作的实现和责任进行分割,把动作委托到代表当前状态的对象。而策略模式是对算法进行封装,把算法的责任和算法本身进行分割开来,同时委派给不同的对象进行管理。策略模式是将一个系列的算法封装到一系列的策略里面。

其实对于算法的选择,策略模式并不关心,它只是对算法进行封装,至于算法什么时候什么地方使用什么算法都是客户所决定的,这样就提高了系统的灵活性,但同时也增加了客户的负担,因为客户需要清楚知道选择什么样的算法对自己最有利,这就需要客户对每一个算法都清楚知道他们的区别。

20.2.2 适用场景
  • 一个系统需要动态地在几种算法中选择一种时,可将每个算法封装到策略类中。
  • 一个类定义了多种行为,并且这些行为在这个类的操作中以多个条件语句的形式出现,可将每个条件分支移入它们各自的策略类中以代替这些条件语句。
  • 系统中各算法彼此完全独立,且要求对客户隐藏具体算法的实现细节时。
  • 系统要求使用算法的客户不应该知道其操作的数据时,可使用策略模式来隐藏与算法相关的数据结构。
  • 多个类只区别在表现行为不同,可以使用策略模式,在运行时动态选择具体要执行的行为。

20.3 实现

我们都知道排序算法有很多种,但是什么时候选择冒泡排序,什么时候选择选择排序,什么时候选择插入排序,所以这里用排序算法来演示策略模式的实现。

/**
 * 抽象策略类
 * 排序类
 */
public interface Sort {

    // 定义公共的方法:排序
    int[] sort(int arr[]);

}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
/**
 * 具体策略类
 * 冒泡排序
 */
public class BubbleSort implements Sort {
    @Override
    public int[] sort(int[] arr) {

        int length = arr.length;

        int i, j, temp;

        for (j = 0; j < length - 1; j++) {
            for (i = 0; i < length - j - 1; i++) {
                if (arr[i] > arr[i + 1]) {
                    temp = arr[i];
                    arr[i] = arr[i + 1];
                    arr[i + 1] = temp;
                }
            }
        }
        System.out.println("冒泡排序:");
        return arr;
    }
}

/**
 * 具体策略类
 * 插入排序
 */
public class InsertionSort implements Sort {
    @Override
    public int[] sort(int[] arr) {
        int length = arr.length;

        for (int i = 1; i < length; i++) {
            int temp = arr[i];
            int j;
            for (j = i; j > 0; j--) {
                if (arr[j - 1] > temp) {
                    arr[j] = arr[j - 1];
                } else break;
            }
            arr[j] = temp;
        }
        System.out.println("插入排序:");
        return arr;
    }
}

/**
 * 具体策略类
 * 选择排序
 */
public class SelectSort implements Sort {
    @Override
    public int[] sort(int[] arr) {
        int len = arr.length;
        int temp;
        for (int i = 0; i < len; i++) {
            temp = arr[i];
            int j;
            int smallestLocation = i;
            for (j = i + 1; j < len; j++) {
                if (arr[j] < temp) {
                    temp = arr[j];
                    smallestLocation = j;
                }
            }
            arr[smallestLocation] = arr[i];
            arr[i] = temp;
        }
        System.out.println("选择排序:");
        return arr;
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
/**
 * 环境类
 * 聚合一个抽象策略类对象,用以调用某个具体的策略
 */
public class ArrayHandler {

    private Sort sortObj;

    // 供客户端进行调用
    public int[] sort(int arr[])
    {
        sortObj.sort(arr);
        return arr;
    }

    // 由客户端进行策略的选择设置
    public void setSortObj(Sort sortObj) {
        this.sortObj = sortObj;
    }

}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
/**
 * 客户端
 */
public class Client {

    public static void main(String args[])
    {
        int arr[]={1,4,6,2,5,3,7,10,9};
        int result[];
        ArrayHandler ah=new ArrayHandler();

        // 缺点就是需要客户端知晓所有的具体算法实现
        Sort sort = new SelectSort();    //使用选择排序

        ah.setSortObj(sort); //设置具体策略
        result=ah.sort(arr);

        for(int i=0;i<result.length;i++)
        {
            System.out.print(result[i] + ",");
        }
    }

}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24

20.4 特点

20.4.1 优点
  • 多重条件语句不易维护,而使用策略模式可以避免使用多重条件语句。

  • 策略模式提供了一系列的可供重用的算法族,恰当使用继承可以把算法族的公共代码转移到父类里面,从而避免重复的代码。

  • 策略模式可以提供相同行为的不同实现,客户可以根据不同时间或空间要求选择不同的。

  • 策略模式提供了对开闭原则的完美支持,可以在不修改原代码的情况下,灵活增加新算法。

  • 策略模式把算法的使用放到环境类中,而算法的实现移到具体策略类中,实现了二者的分离。

20.4.2 缺点
  • 客户端必须理解所有策略算法的区别,以便适时选择恰当的算法类

  • 策略模式造成很多的策略类

  • 客户端必须知道所有的策略类,并自行决定使用哪一个策略类

20.5 总结

  • 策略模式是一个比较容易理解和使用的设计模式,策略模式是对算法的封装,它把算法的责任和算法本身分割开,委派给不同的对象管理。策略模式通常把一个系列的算法封装到一系列的策略类里面,作为一个抽象策略类的子类。用一句话来说,就是“准备一组算法,并将每一个算法封装起来,使得它们可以互换”。
  • 在策略模式中,应当由客户端自己决定在什么情况下使用什么具体策略角色。
  • 策略模式仅仅封装算法,提供新算法插入到已有系统中,以及老算法从系统中“退休”的方便,策略模式并不决定在何时使用何种算法,算法的选择由客户端来决定。这在一定程度上提高了系统的灵活性,但是客户端需要理解所有具体策略类之间的区别,以便选择合适的算法,这也是策略模式的缺点之一,在一定程度上增加了客户端的使用难度。

二十一、模板方法模式

定义一个操作中的算法骨架,而将算法的一些步骤延迟到子类中,使得子类可以在不改变该算法结构的情况下重定义该算法的某些特定步骤。它是一种类行为型模式。

21.1 背景

在面向对象开发过程中,通常我们会遇到这样的一个问题:我们知道一个算法所需的关键步骤,并确定了这些步骤的执行顺序。但是某些步骤的具体实现是未知的,或者说某些步骤的实现与具体的环境相关。
例子1:银行业务办理流程
在银行办理业务时,一般都包含几个基本固定步骤:取号排队->办理具体业务->对银行工作人员进行评分。
取号取号排队和对银行工作人员进行评分业务逻辑是一样的。但是办理具体业务是不相同的,具体业务可能取款、存款或者转账。

这样的例子在生活中还有很多,例如,一个人每天会起床、吃饭、做事、睡觉等,其中“做事”的内容每天可能不同。我们把这些规定了流程或格式的实例定义成模板,允许使用者根据自己的需求去更新它,例如,简历模板、论文模板、Word 中模板文件等。

21.2 概述

所谓模板方法模式就是在一个方法中定义一个算法的骨架,而将一些步骤延迟到子类中。模板方法使得子类可以在不改变算法结构的情况下,重新定义算法中的某些步骤。

模板方法模式是基于继承的代码复用技术的。在模板方法模式中,我们可以将相同部分的代码放在父类中,而将不同的代码放入不同的子类中。也就是说我们需要声明一个抽象的父类,将部分逻辑以具体方法以及具体构造函数的形式实现,然后声明一些抽象方法让子类来实现剩余的逻辑,不同的子类可以以不同的方式来实现这些逻辑。

其实所谓模板就是一个方法,这个方法将算法的实现定义成了一组步骤,其中任何步骤都是可以抽象的,交由子类来负责实现。这样就可以保证算法的结构保持不变,同时由子类提供部分实现。

模板是一个方法,那么他与普通的方法存在什么不同呢?模板方法是定义在抽象类中,把基本操作方法组合在一起形成一个总算法或者一组步骤的方法。而普通的方法是实现各个步骤的方法,我们可以认为普通方法是模板方法的一个组成部分。

21.2.1 类图

image-20200924093351014

  • **抽象类(AbstractClass):**负责给出一个算法的轮廓和骨架。它由一个模板方法和若干个基本方法构成。这些方法的定义如下:

    • 模板方法:定义了算法的骨架,按某种顺序调用其包含的基本方法。
    • 基本方法:是整个算法中的一个步骤,包含以下几种类型。
    • 抽象方法:在抽象类中申明,由具体子类实现。
    • 具体方法:在抽象类中已经实现,在具体子类中可以继承或重写它。
    • 钩子方法:在抽象类中已经实现,包括用于判断的逻辑方法和需要子类重写的空方法两种。

    钩子是一种被声明在抽象类中的方法,但只有空的或者默认的实现。钩子的存在可以使子类能够对算法的不同点进行挂钩,即让子类能够对模板方法中某些即将发生变化的步骤做出相应的反应。当然要不要挂钩,由子类决定。

  • **具体子类(ConcreteClass):**实现抽象类中所定义的抽象方法和钩子方法,它们是一个顶级逻辑的一个组成步骤。

21.2.2 适用场景
  1. 算法的整体步骤很固定,但其中个别部分易变时,这时候可以使用模板方法模式,将容易变的部分抽象出来,供子类实现。 一次性实现一个算法的不变的部分,并将可变的行为留给子类来实现。
  2. 当多个子类存在公共的行为时,可以将其提取出来并集中到一个公共父类中以避免代码重复。首先,要识别现有代码中的不同之处,并且将不同之处分离为新的操作。最后,用一个调用这些新的操作的模板方法来替换这些不同的代码。
  3. 控制子类扩展。当需要控制子类的扩展时,模板方法只在特定点调用钩子操作,这样就只允许在这些点进行扩展。

21.3 实现

冲咖啡和泡茶:

image-20200924094405387

将图中需要变化的部分取出并“封装”起来,即我们需要将某些相同代码抽象出来。根据这个原则我们可以确认这个应用中存在两个不变的部分:把水煮沸、把茶倒入杯子中,需要变化的有:用沸水冲泡咖啡(茶)、加入糖和牛奶(加入柠檬)。所以这里需要将这个变化部分抽象出来,交由子类去实现。

泡咖啡和泡茶共用一个相同的泡法:把水煮沸——>用沸水冲泡——>倒入杯子中——>加入调料。

/**
 * 抽象类
 */
public abstract class BeverageMixer {

    /**
     * @return void
     * @desc 模板方法,用来控制泡茶与冲咖啡的流程
     * 申明为final,不希望子类覆盖这个方法,防止更改流程的执行顺序
     */
    public final void prepareRecipe() {
        boilWater();
        brew();
        pourInCup();
        if (customerWantsCondiments())
            addCondiments();
    }

    /**
     * 钩子方法
     *
     * @return void
     * @desc 将brew()、addCondiment()声明为抽象类,具体操作由子类实现
     */
    // 冲泡
    protected abstract void brew();

    // 添加调味品
    protected abstract void addCondiments();

    // 是否要添加调料
    public boolean customerWantsCondiments() {
        return true;
    }

    // 客户输入y/n
    protected static String getString() {
        String answer = null;
        BufferedReader in = new BufferedReader(new InputStreamReader(System.in));
        try {
            answer = in.readLine();
        } catch (IOException e) {
            e.printStackTrace();
        }
        if (answer == null) {
            return "n";
        }
        return answer;
    }

    // 不标注作用域符,是不可被子类继承的
    // 公共部分
    //1 煮开水
    void boilWater() {
        System.out.println("烧水...");
    }

    //2 将水倒入杯中
    void pourInCup() {
        System.out.println("将水倒入杯中...");
    }

}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
/**
 * 具体类
 * 实现具体的钩子方法
 */
public class Coffee extends BeverageMixer {
    @Override
    protected void brew() {
        System.out.println("将咖啡放入杯中...");
    }

    @Override
    protected void addCondiments() {
        System.out.println("添加糖和牛奶...");
    }

    /**
     * 覆盖该钩子,提供自己的实现方法
     */
    public boolean customerWantsCondiments() {
        if ("y".equals(getUserInput().toLowerCase())) {
            return true;
        } else {
            return false;
        }
    }

    // 客户输入是否要添加调料
    public String getUserInput() {
        System.out.print("Would you like milk and sugar with your coffee(y/n):");
        return getString();
    }
}


/**
 * 具体类
 * 实现具体的钩子方法
 */
public class Tea extends BeverageMixer {
    @Override
    protected void brew() {
        System.out.println("将茶放入杯中...");
    }

    @Override
    protected void addCondiments() {
        System.out.println("放入柠檬...");
    }

    /**
     * 覆盖该钩子,提供自己的实现方法
     */
    public boolean customerWantsCondiments() {
        if ("y".equals(getUserInput().toLowerCase())) {
            return true;
        } else {
            return false;
        }
    }

    // 客户输入是否要添加调料
    public String getUserInput() {
        System.out.print("Would you like milk and sugar with your tea(y/n):");
        return getString();

    }
    
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
/**
 * 客户端
 */
public class Client {

    public static void main(String[] args) {

        BeverageMixer tea = new Tea();
        tea.prepareRecipe();

        BeverageMixer coffee = new Coffee();
        coffee.prepareRecipe();
    }

}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

21.4 特点

21.4.1 优点
  1. 它封装了不变部分,扩展可变部分。它把认为是不变部分的算法封装到父类中实现,而把可变部分算法由子类继承实现,便于子类继续扩展。
  2. 它在父类中提取了公共的部分代码,便于代码复用。
  3. 部分方法是由子类实现的,因此子类可以通过扩展方式增加相应的功能,符合开闭原则。
21.4.2 缺点
  1. 对每个不同的实现都需要定义一个子类,这会导致类的个数增加,系统更加庞大,设计也更加抽象。
  2. 父类中的抽象方法由子类实现,子类执行的结果会影响父类的结果,这导致一种反向的控制结构,它提高了代码阅读的难度。

21.5 总结

模板方法模式的抽象类定义方法:

模板方法: 一个模板方法是 定义在抽象类中的、把基本操作方法组合在一起 形成一个总算法或一个总行为的方法。
基本方法: 基本方法是 实现算法各个步骤的方法 ,是模板方法的组成部分。 基本方法如下:

  • 抽象方法 (Abstract Method)
  • 具体方法 (Concrete Method)
  • 钩子方法 (Hook Method) :“挂钩”方法和空方法,

hook方法在抽象类中的实现为空,是留给子类做一些可选的操作。如果某个子类需要一些特殊额外的操作,则可以实现hook方法,当然也可以完全不用理会,因为hook在抽象类中只是空方法而已。
1)钩子方法的引入使得子类可以控制父类的行为。
2)最简单的钩子方法就是 空方法,也可以在钩子方法中定义一个默认的实现 ,如果子类不覆盖钩子方法,则执行父类的默认实现代码。
3)比较复杂一点的钩子方法 可以对其他方法进行约束 ,这种钩子方法通常返回一个 boolean 类型,即返回 true 或 false ,用来判断是否执行某一个基本方法。由子类来决定是否调用hook方法。


  • 模板方法模式是一种类的行为型模式,在它的结构图中 只有类之间的继承关系,没有对象关联关系 。

  • 模板方法模式是 基于继承 的代码复用基本技术,模板方法模式的结构和用法也是面向对象设计的核心之一。在模板方法模式中,可以 将相同的代码放在父类中,而将不同的方法实现放在不同的子类中 。

  • 在模板方法模式中,我们需要准备一个抽象类, 将部分逻辑以具体方法以及具体构造函数的形式实现 ,然后 声明一些抽象方法来让子类实现剩余的逻辑 。 不同的子类可以以不同的方式实现这些抽象方法,从而对剩余的逻辑有不同的实现, 这就是模板方法模式的用意。模板方法模式体现了面向对象的诸多重要思想,是一种使用频率较高的模式。

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/我家自动化/article/detail/525972
推荐阅读
相关标签
  

闽ICP备14008679号