设计模式07 行为型模式1-模板方法+命令+访问者

467 阅读16分钟

完整设计模式学习笔记请戳

13. 模板方法模式

豆浆制作问题
编写制作豆浆的程序,说明如下:

  1. 制作豆浆的流程 选材--->添加配料--->浸泡--->放到豆浆机打碎
  2. 通过添加不同的配料,可以制作出不同口味的豆浆
  3. 选材、浸泡和放到豆浆机打碎这几个步骤对于制作每种口味的豆浆都是一样的
  4. 请使用 模板方法模式完成

基本介绍

  1. 模板方法模式(Template Method Pattern),又叫模板模式(Template Pattern),在一个抽象类公开定义了执行它的方法的模板。它的子类可以按需要重写方法实现,但调用将以抽象类中定义的方式进行。
  2. 简单说,模板方法模式定义一个操作中的算法的骨架,而将一些步骤延迟到子类中,使得子类可以不改变一个算法的结构,就可以重定义该算法的某些特定步骤
  3. 这种类型的设计模式属于行为型模式。
1

对原理类图的说明-即(模板方法模式的角色及职责)

  1. AbstractClass 抽象类, 类中实现了模板方法(template),定义了算法的骨架,具体子类需要去实现 其它的抽象方法 operationr2,3,4
  2. ConcreteClass 实现抽象方法 operationr2,3,4, 以完成算法中特点子类的步骤

代码实现

  1. 应用实例要求 编写制作豆浆的程序,说明如下: 制作豆浆的流程 选材--->添加配料--->浸泡--->放到豆浆机打碎通过添加不同的配料,可以制作出不同口味的豆浆 选材、浸泡和放到豆浆机打碎这几个步骤对于制作每种口味的豆浆都是一样的(红豆、花生豆浆。。。)
  2. 思路分析和图解(类图)
    2
  • 抽象类,豆浆
public abstract class SoyaMilk {
    //模板方法 make
    final void make(){
        select();
        addCondiments();
        soak();
        beat();
    }

    //选材
    void select(){
        System.out.println("第一步,选择好的新鲜黄豆");
    }

    //添加不同配料,子类具体实现
    abstract void addCondiments();

    //浸泡
     void soak(){
        System.out.println("第三步,黄豆和配料开始浸泡,需要3个小时");
    }

    //打豆浆
    void beat(){
        System.out.println("第四步,黄豆和配料放到豆浆机去打碎");
    }

}
  • 红豆豆浆和花生豆浆,某些具体细节不同
public class RedBeanSoyaMilk extends SoyaMilk {
    @Override
    void addCondiments() {
        System.out.println("第二步,加入上等红豆");
    }
}

public class PeanutSoyaMilk extends SoyaMilk {
    @Override
    void addCondiments() {
        System.out.println("第二步,加入上好的花生");
    }
}
  • 客户端调用
public class Client {
    public static void main(String[] args) {
        System.out.println("--------制作红豆豆浆--------");
        SoyaMilk soyaMilk = new RedBeanSoyaMilk();
        soyaMilk.make();
        System.out.println("--------制作红豆豆浆--------");
        SoyaMilk peanutSoyaMilk = new PeanutSoyaMilk();
        peanutSoyaMilk.make();
    }
}

模板方法模式的钩子方法

  1. 在模板方法模式的父类中,我们可以定义一个方法,它默认不做任何事,子类可以视情况要不要覆盖它,该方法称为“钩子”。
  2. 还是用上面做豆浆的例子来讲解,比如,我们还希望制作纯豆浆,不添加任何的配料,请使用钩子方法对前面的模板方法进行改造
  • 改造抽象类
public abstract class SoyaMilk {
    //模板方法 make
    final void make(){
        select();
        if (customerWantCondiments()) {
            addCondiments();
        }
        soak();
        beat();
    }
    
    ...
    
    //钩子方法,决定是否需要添加配料
    boolean customerWantCondiments() {
        return true;
    }
}
  • 纯豆浆
public class PureSoyaMilk extends SoyaMilk {
    @Override
    void addCondiments() {

    }

    //重写钩子方法
    @Override
    boolean customerWantCondiments() {
        return false;
    }
}
  • 客户端调用
main(...){
    System.out.println("--------制作纯豆浆--------");
    SoyaMilk pureSoyaMilk = new PureSoyaMilk();
    pureSoyaMilk.make();
    
    //输出
    --------制作纯豆浆--------
    第一步,选择好的新鲜黄豆
    第三步,黄豆和配料开始浸泡,需要3个小时
    第四步,黄豆和配料放到豆浆机去打碎
}

模板方法的应用:Android中View的draw

Android中View的draw方法就是使用了模板方法模式:

public class View{
    //钩子方法,空实现
    protected void onDraw(Canvas canvas) {
    }
    //钩子方法,空实现
    protected void dispatchDraw(Canvas canvas) {
    }
    //绘制方法,定义绘制流程
    public void draw(Canvas canvas) {
       //其他代码略

        /*
         *  绘制流程如下:
         *
         *      1. 绘制view背景
         *      2. 如果有需要,就保存图层
         *      3. 绘制view内容
         *      4. 绘制子View
         *      5. 如果有必要,绘制渐变框和恢复图层
         *      6. 绘制装饰(滑动条等)
         */

        if (!dirtyOpaque) {
            drawBackground(canvas);//步骤1. 绘制view背景
        }

        // 如果可能的话跳过第2步和第5步(常见情况)
        final int viewFlags = mViewFlags;
        boolean horizontalEdges = (viewFlags & FADING_EDGE_HORIZONTAL) != 0;
        boolean verticalEdges = (viewFlags & FADING_EDGE_VERTICAL) != 0;
        if (!verticalEdges && !horizontalEdges) {
            if (!dirtyOpaque) onDraw(canvas);//步骤3. 绘制view内容

            dispatchDraw(canvas);//步骤4. 绘制子View

            // 覆盖一部分内容,绘制前景
            if (mOverlay != null && !mOverlay.isEmpty()) {
                mOverlay.getOverlayView().dispatchDraw(canvas);
            }

            onDrawForeground(canvas); //步骤6. 绘制装饰(滑动条等)

            return;
        }
}

说明:

  • View的draw()方法中定义了一整套的绘制流程,这个流程是固定的,所有的Android中的View都是按照这个流程来绘制的。其中drawBackground()这个方法在View类中是实现了具体过程的,而onDraw()方法和dispatchDraw()方法在View中都是空实现,即都是钩子方法。不同的子类通过重写这些空实现来实现自身不同的绘制效果。
  • 具体的View,像TextView这些单一的View,就会重写onDraw()方法,由于TextView没有子View,所以dispatchDraw()还是空实现;而ViewGroup类含有子View,需要遍历子View并绘制,因此需要重写onDraw()和dispatchDraw()。
  • 所以,我们自定义View时必须且只需重写onDraw();自定义ViewGroup时则需要重写onDraw()和dispatchDraw()。

模板方法模式在 Spring 框架应用的源码分析

3

模板方法模式的注意事项和细节

  1. 基本思想是:算法只存在于一个地方,也就是在父类中,容易修改。需要修改算法时,只要修改父类的模板方法或者已经实现的某些步骤,子类就会继承这些修改
  2. 实现了最大化代码复用。父类的模板方法和已实现的某些步骤会被子类继承而直接使用。
  3. 既统一了算法,也提供了很大的灵活性。父类的模板方法确保了算法的结构保持不变,同时由子类提供部分步骤的实现。
  4. 该模式的不足之处:每一个不同的实现都需要一个子类实现,导致类的个数增加,使得系统更加庞大
  5. 一般模板方法都加上 final 关键字, 防止子类重写模板方法.
  6. 模板方法模式使用场景:当要完成在某个过程,该过程要执行一系列步骤 ,这一系列的步骤基本相同,但其个别步骤在实现时可能不同,通常考虑用模板方法模式来处理

14.命令模式

需求

  1. 我们买了一套智能家电,有照明灯、风扇、冰箱、洗衣机,我们只要在手机上安装 app 就可以控制对这些家电工作。
  2. 这些智能家电来自不同的厂家,我们不想针对每一种家电都安装一个 App,分别控制,我们希望只要一个 app 就可以控制全部智能家电。
  3. 要实现一个 app 控制所有智能家电的需要,则每个智能家电厂家都要提供一个统一的接口给 app 调用,这时 就可以考虑使用命令模式。
  4. 命令模式可将“动作的请求者”从“动作的执行者”对象中解耦出来.
  5. 在我们的例子中,动作的请求者是手机 app,动作的执行者是每个厂商的一个家电产品

基本介绍

  1. 命令模式(Command Pattern):在软件设计中,我们经常需要向某些对象发送请求,但是并不知道请求的接收者是谁,也不知道被请求的操作是哪个, 我们只需在程序运行时指定具体的请求接收者即可,此时,可以使用命令模式来进行设计
  2. 命名模式使得请求发送者与请求接收者消除彼此之间的耦合,让对象之间的调用关系更加灵活,实现解耦。
  3. 在命令模式中,会将一个请求封装为一个对象,以便使用不同参数来表示不同的请求(即命名),同时命令模式也支持可撤销的操作。
  4. 通俗易懂的理解:将军发布命令,士兵去执行。其中有几个角色:将军(命令发布者)、士兵(命令的具体执行者)、命令(连接将军和士兵)。 Invoker 是调用者(将军),Receiver 是被调用者(士兵),MyCommand 是命令,实现了 Command 接口,持有接收对象
4

对原理类图的说明-即(命名模式的角色及职责)

  1. Invoker 是调用者角色
  2. Command: 是命令角色,需要执行的所有命令都在这里,可以是接口或抽象类
  3. Receiver: 接收者角色,知道如何实施和执行一个请求相关的操作
  4. ConcreteCommand: 将一个接受者对象与一个动作绑定,调用接受者相应的操作,实现 execute

代码实现

5 6
  1. ICommand 抽象命令者(Command角色)
public interface ICommand {
    void execute();//执行
    void undo();//撤销
}
  1. 电灯开关命令(ConcreteCommand)
public class LightOnCommand implements ICommand {
    LightReceiver receiver;

    public LightOnCommand(LightReceiver receiver) {
        this.receiver = receiver;
    }

    @Override
    public void execute() {
        receiver.on();
    }

    @Override
    public void undo() {
        receiver.off();
    }
}
public class LightOffCommand implements ICommand {
    LightReceiver receiver;

    public LightOffCommand(LightReceiver receiver) {
        this.receiver = receiver;
    }

    @Override
    public void execute() {
        receiver.off();
    }

    @Override
    public void undo() {
        receiver.on();
    }
}
public class NoCommand implements ICommand {
    @Override
    public void execute() {
    }
    @Override
    public void undo() {

    }
}
  1. 电灯对象 (接收者 Receiver角色)
public class LightReceiver {

    public void on(){
        System.out.println("开灯请睁眼");
    }
    public void off(){
        System.out.println("关灯请闭眼");
    }
}
  1. 遥控器(调用者角色 Invoker角色)
public class RemoteController {
    //开关按钮
    ICommand[] onCommands;
    ICommand[] offCommands;
    //撤销按钮
    ICommand undoCommand;

    public RemoteController() {
        onCommands  = new ICommand[5];
        offCommands  = new ICommand[5];
        for (int i = 0; i < 5; i++) {
            onCommands[i] = new NoCommand();
            offCommands[i] = new NoCommand();
        }

    }

    // 给我们的按钮设置你需要的命令
    public void setCommand(int no, ICommand onCommand, ICommand offCommand) {
        onCommands[no] = onCommand;
        offCommands[no] = offCommand;
    }

    public void onButtonClicked(int no){
        onCommands[no].execute();
        undoCommand = onCommands[no];
    }

    public void offButtonClicked(int no){
        offCommands[no].execute();
        undoCommand = offCommands[no];
    }

    public void undoButtonClicked(){
        undoCommand.undo();
    }
    
}
  1. 客户端调用
public class Client {
    public static void main(String[] args) {
        //创建电灯的对象 (接收者)
        LightReceiver receiver = new LightReceiver();
        //创建电灯的开关命令
        ICommand onCommand = new LightOnCommand(receiver);
        ICommand offCommand = new LightOffCommand(receiver);
        //创建遥控器
        RemoteController controller = new RemoteController();
        //给遥控器设置相关命令
        controller.setCommand(0,onCommand,offCommand);
        System.out.println("----按下开灯按钮----");
        controller.onButtonClicked(0);
        System.out.println("----按下关灯按钮----");
        controller.offButtonClicked(0);
        System.out.println("----按下撤销按钮----");
        controller.undoButtonClicked();

        System.out.println("----------开始操作电视机----------");
        TVReceiver tvReceiver = new TVReceiver();
        ICommand tvOnCommand = new TVOnCommand(tvReceiver);
        ICommand tvOffCommand = new TVOffCommand(tvReceiver);
        controller.setCommand(1,tvOnCommand,tvOffCommand);
        controller.onButtonClicked(1);
        controller.offButtonClicked(1);
        controller.undoButtonClicked();

    }
}

命令模式应用

Thread

实际上Thread的使用就是一个简单的命令模式,先看下Thread的使用:

 new Thread(new Runnable() {
            @Override
            public void run() {
                //doSomeThing
            }
        }).start();

Thread的start()方法即命令的调用者,同时Thread的内部会调用Runnable的run(),这里Thread又充当了具体的命令角色,最后的Runnable则是接受者了,负责最后的功能处理。

Android 中的 Handler

另一个比较典型的常用到命令模式就是Handler了,这里就不贴代码了,简单分析下各个类的角色:

  • 接收者:Handler,执行消息的处理操作。
  • 调用者:Looper,调用消息的的处理方法。
  • 命令角色:Message,消息类。

命令模式的注意事项和细节

  1. 将发起请求的对象与执行请求的对象解耦。发起请求的对象是调用者,调用者只要调用命令对象的 execute()方法就可以让接收者工作,而不必知道具体的接收者对象是谁、是如何实现的,命令对象会负责让接收者执行请求的动作,也就是说:”请求发起者”和“请求执行者”之间的解耦是通过命令对象实现的,命令对象起到了纽带桥梁的作用。
  2. 容易设计一个命令队列。只要把命令对象放到列队,就可以多线程的执行命令
  3. 容易实现对请求的撤销和重做
  4. 命令模式不足:可能导致某些系统有过多的具体命令类,增加了系统的复杂度,这点在在使用的时候要注意
  5. 空命令也是一种设计模式,它为我们省去了判空的操作。在上面的实例中,如果没有用空命令,我们每按下一个按键都要判空,这给我们编码带来一定的麻烦。
  6. 命令模式经典的应用场景:界面的一个按钮都是一条命令、模拟 CMD(DOS 命令)订单的撤销/恢复、触发- 反馈机制

15. 访问者模式

需求
完成测评系统需求

  1. 将观众分为男人和女人,对歌手进行测评,当看完某个歌手表演后,得到他们对该歌手不同的评价(评价 有不同的种类,比如 成功、失败 等)
  2. 传统方案

传统方式的问题分析

  1. 如果系统比较小,还是 ok 的,但是考虑系统增加越来越多新的功能时,对代码改动较大,违反了 ocp 原则, 不利于维护
  2. 扩展性不好,比如 增加了 新的人员类型,或者管理方法,都不好做
  3. 引出我们会使用新的设计模式 – 访问者模式

基本介绍

  1. 访问者模式(Visitor Pattern),封装一些作用于某种数据结构的各元素的操作,它可以在不改变数据结构的前提下定义作用于这些元素的新的操作。
  2. 主要将数据结构与数据操作分离,解决 数据结构和操作耦合性问题
  3. 访问者模式的基本工作原理是:在被访问的类里面加一个对外提供接待访问者的接口
  4. 访问者模式主要应用场景是:需要对一个对象结构中的对象进行很多不同操作(这些操作彼此没有关联),同时需要避免让这些操作"污染"这些对象的类,可以选用访问者模式解决
7

对原理类图的说明-即(访问者模式的角色及职责)

  1. Visitor 是抽象访问者,为该对象结构中的 ConcreteElement 的每一个类声明一个 visit 操作
  2. ConcreteVisitor :是一个具体的访问值 实现每个有 Visitor 声明的操作,是每个操作实现的部分.
  3. ObjectStructure 能枚举它的元素, 可以提供一个高层的接口,用来允许访问者访问元素
  4. Element 定义一个 accept 方法,接收一个访问者对象
  5. ConcreteElement 为具体元素,实现了 accept 方法

代码实现

  1. 将人分为男人和女人,对歌手进行测评,当看完某个歌手表演后,得到他们对该歌手不同的评价(评价 有不同的种类,比如 成功、失败 等),请使用访问者模式来说实现
  2. 思路分析和图解(类图)
    8

① 抽象访问者(Visitor角色)

public abstract class Action {
    //得到男性测评结果
    abstract void getManResult(Man man);
    //得到女性评测结果
    abstract void getWomanResult(Woman man);
}

②具体访问者 (ConcreteVisitor角色)

public class Success extends Action{
    @Override
    void getManResult(Man man) {
        System.out.println(man.getName()+",这个男人给的评价是很成功");
    }

    @Override
    void getWomanResult(Woman woman) {
        System.out.println(woman.getName()+",这个女人给的评价是很成功");
    }
}

public class Fail extends Action{
    @Override
    void getManResult(Man man) {
        System.out.println(man.getName()+",这个男人给的评价是失败了");
    }

    @Override
    void getWomanResult(Woman woman) {
        System.out.println(woman.getName()+",这个女人给的评价是失败了");
    }
}

③ 抽象接收者(Element角色)

public abstract class Person {
    //提供一个方法 让访问者可以访问
    public abstract void accept(Action action);
}

④ 具体接收者

/**
 * @author DSH
 * 说明:
 * 1. 这里使用到了双分派,即首先在客户端程序中,将具体的状态作为参数传递到Woman或者Man中(第一次分派)
 * 2. 然后Woman类调用了作为参数的"具体方法"中方法getWomanResult,同时将自己(this)作为参数传入,完成了第二次分派
 */
public class Woman extends Person {
    @Override
    public void accept(Action action) {
        action.getWomanResult(this);
    }
}

public class Man extends Person {

    @Override
    public void accept(Action action) {
        action.getManResult(this);
    }
}

⑤ 数据结构 (ObjectStructure 角色)

/**
 * @description 数据结构,管理很多人(Man  Woman)
 */
public class ObjectStructure {
    //维护了一个集合
    private List<Person> persons = new LinkedList<>();

    //增加到list
    public void attach(Person person){
        persons.add(person);
    }

    //删除
    public void detach(Person person){
        persons.remove(person);
    }

    //显示测评结果
    public void display(Action action){
        for (Person p: persons) {
            p.accept(action);
        }
    }
}

⑥ 客户端调用

public class Client {
    public static void main(String[] args) {
        //创建objectStructure
        ObjectStructure objectStructure = new ObjectStructure();
        objectStructure.attach(new Man("张三"));
        objectStructure.attach(new Man("李四"));
        objectStructure.attach(new Woman("王小花"));
        //成功
        Action success = new Success();
        objectStructure.display(success);

        //失败
        Action fail = new Fail();
        objectStructure.display(fail);

    }
}

输出

张三,这个男人给的评价是很成功
李四,这个男人给的评价是很成功
王小花,这个女人给的评价是很成功
张三,这个男人给的评价是失败了
李四,这个男人给的评价是失败了
王小花,这个女人给的评价是失败了
张三,这个男人给的评价是待定
李四,这个男人给的评价是待定
王小花,这个女人给的评价是待定

双分派

  • 上面提到了双分派,所谓双分派是指不管类怎么变化,我们都能找到期望的方法运行。双分派意味着得到执行的操作取决于请求的种类和两个接收者的类型
  • 以上述实例为例,假设我们要添加一个 Wait 的状态类,考察 Man 类和 Woman 类的反应,由于使用了双分派,只需增加一个 Action 子类即可在客户端调用即可,不需要改动任何其他类的代码

如: 新增待定评价

public class Wait extends Action {
    @Override
    void getManResult(Man man) {
        System.out.println(man.getName()+",这个男人给的评价是待定");
    }

    @Override
    void getWomanResult(Woman woman) {
        System.out.println(woman.getName()+",这个女人给的评价是待定");
    }
}

调用

main(...){
    //待定
    Action wait = new Wait();
    objectStructure.display(wait);
}

访问者模式的注意事项和细节

优点

  1. 访问者模式符合单一职责原则、让程序具有优秀的扩展性、灵活性非常高
  2. 访问者模式可以对功能进行统一,可以做报表、UI、拦截器与过滤器,适用于数据结构相对稳定的系统

缺点

  1. 具体元素对访问者公布细节,也就是说访问者关注了其他类的内部细节,这是迪米特法则所不建议的, 这样造成了具体元素变更比较困难
  2. 违背了依赖倒转原则。访问者依赖的是具体元素,而不是抽象元素
  3. 因此,如果一个系统有比较稳定的数据结构,又有经常变化的功能需求,那么访问者模式就是比较合适的.


设计模式学习代码及笔记)

【代码】
github.com/willShuhuan…
【笔记】
设计模式01 七大原则
设计模式02 类关系与UML类图
设计模式03 创建型模式1-单例+工厂
设计模式04 创建型模式2-原型+建造者
设计模式05 结构型模式1-适配器+桥接+装饰者
设计模式06 结构型模式2-组合+外观+享元+代理
设计模式07 行为型模式1-模板方法+命令+访问者
设计模式08 行为型模式2-迭代器+观察者+中介者
设计模式09 行为型模式3-备忘录+解释器+状态
设计模式10 行为型模式4-策略+职责链