命令模式与访问者模式

命令模式与访问者模式

参考教程:https://www.bilibili.com/video/BV1G4411c7N4
代码实现 Github:https://github.com/yaokuku123/pattern


命令模式

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

解释:在软件设计中,我们经常需要 向某些对象发送请求,但是并不知道请求的接收者是谁,也不知 道被请求的操作是哪个。我们只需在程序运行时指定具体的请求接收者即可,此时,可以 使用命令模式来进行设计。

通俗易懂的理解:将军发布命令,士兵去执行。其中有几个角色: 将军(命令发布者)、士兵(命令的具体执行者)、命令(连接将军和士兵)。

  1. 代码实现
1-1592895044540.png

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

  1. Invoker 是调用者角色

  2. Command: 是命令角色,需要执行的所有命令都在这里,可以是接口或抽象类

  3. Receiver: 接受者角色,知道如何实施和执行一个请求相关的操作

  4. ConcreteCommand: 将一个接受者对象与一个动作绑定,调用接受者相应的操作,实现execute

2-1592895144439.png
package com.yqj.pattern.command;

//具体执行者
class LightReceiver{
    public void on(){
        System.out.println("开灯");
    }

    public void off(){
        System.out.println("关灯");
    }
}

//命令接口
interface Command{
    //执行命令
    public void execute();
    //撤销操作
    public void undo();
}

//具体命令,开灯
class LightOnCommand implements Command{
    //聚合具体的执行者
    private LightReceiver lightReceiver;
    //传入执行者
    public LightOnCommand(LightReceiver lightReceiver) {
        this.lightReceiver = lightReceiver;
    }

    @Override
    public void execute() {
        //调用执行者的方法
        lightReceiver.on();
    }

    @Override
    public void undo() {
        //调用执行者的方法
        lightReceiver.off();
    }
}

//具体命令,关灯
class LightOffCommand implements Command{

    private LightReceiver lightReceiver;

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

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

    @Override
    public void undo() {
        lightReceiver.on();
    }
}

//具体命令,空实现,方便初始化按钮。调用的时候省掉对空的判断
class NoCommand implements Command{

    @Override
    public void execute() {

    }

    @Override
    public void undo() {

    }
}

class RemoteController{
    //按钮命令数值
    private Command[] onCommands;
    private Command[] offCommands;
    //撤销命令,记录上次的操作
    private Command undoCommand;

    public RemoteController() {
        //初始化按钮
        onCommands = new Command[5];
        offCommands = new Command[5];
        undoCommand = new NoCommand();
        for (int i=0 ; i<5 ; i++){
           onCommands[i] = new NoCommand();
           offCommands[i] = new NoCommand();
        }
    }

    //给按钮设置具体的命令
    public void setCommand(int index,Command onCommand,Command offCommand){
        onCommands[index] = onCommand;
        offCommands[index] = offCommand;
    }

    //按下开按钮
    public void onButtonWasPushed(int index){
        //找到按下的按钮,并调用该按钮的方法
        onCommands[index].execute();
        //记录操作,用于撤销
        undoCommand = onCommands[index];
    }

    //按下关按钮
    public void offButtonWasPushed(int index){
        offCommands[index].execute();
        undoCommand = offCommands[index];
    }

    //按撤销按钮
    public void undoButtonWasPushed(){
        undoCommand.undo();
    }
}

public class Client {
    public static void main(String[] args) {
        //创建电灯对象(执行者)
        LightReceiver lightReceiver = new LightReceiver();
        //创建开,关灯的命令对象
        LightOnCommand lightOnCommand = new LightOnCommand(lightReceiver);
        LightOffCommand lightOffCommand = new LightOffCommand(lightReceiver);
        //创建遥控器
        RemoteController remoteController = new RemoteController();
        //设置按钮对应的命令
        remoteController.setCommand(0,lightOnCommand,lightOffCommand);
        //按下按钮
        remoteController.onButtonWasPushed(0);
        remoteController.offButtonWasPushed(0);
        remoteController.undoButtonWasPushed();
    }
}
  1. 小结
  • 将发起请求的对象与执行请求的对象解耦。发起请求的对象是调用者,调用者只要调用命令对象的execute()方法就可以让接收者工作,而不必知道具体的接收者对象是谁、是如何实现的,命令对象会负责让接收者执行请求的动作,也就是说:” 请求发起者”和“请求执行者”之间的解耦是通过命令对象实现的,命令对象起到了纽带桥梁的作用。
  • 容易设计一个命令队列。只要把命令对象放到列队,就可以多线程的执行命令
  • 容易实现对请求的撤销和重做
  • 命令模式不足:可能导致某些系统有过多的具体命令类,增加了系统的复杂度,这点在在使用的时候要注意
  • 空命令也是一种设计模式,它为我们省去了判空的操作。在上面的实例中,如果没有用空命令,我们每按下一个按键都要判空,这给我们编码带来一定的麻烦。
  • 命令模式经典的应用场景:界面的一个按钮都是一条命令、模拟CMD(DOS命令) 订单的撤销/恢复、触发-反馈机制

访问者模式

  1. 案例

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

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

解释:封装一些作用于某种数据结构的各元素的操作, 它可以在不改变数据结构的前提下定义作用于这些元素的新的操作。主要将数据结构与数据操作分离,解决数据结构和操作耦合性问题。

原理:在被访问的类里面加一个对外提供接待访问者的接口。

场景:需要对一个对象结构中的对象进行很多不同操作 (这些操作彼此没有关联),同时需要避免让这些操作"污染"这些对象的类,可以选用访问者模式解决。

  1. 代码实现
3-1592897022379.png

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

  1. Visitor 是抽象访问者,为该对象结构中的ConcreteElement的每一个类声明一个visit操作

  2. ConcreteVisitor :是一个具体的访问者实现每个有Visitor声明的操作,是每个操作实现的部分.

  3. ObjectStructure 能枚举它的元素,可以提供一个高层的接口,用来允许访问者访问元素

  4. Element 定义一个accept 方法,接收一个访问者对象

  5. ConcreteElement 为具体元素,实现了accept 方法

4-1592897205753.png
package com.yqj.pattern.visitor;

import java.util.ArrayList;
import java.util.List;

abstract class People{
    private String name;

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

    public String getName() {
        return name;
    }

    public abstract void accept(Action action);
}

class Man extends People{

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

    //双分派,首先将具体状态action作为参数传递到man中(第一次分派)
    //然后man类调用作为参数的action的具体方法getManResult(),同时将自己this作为参数传入(第二次分派)
    @Override
    public void accept(Action action) {
        action.getManResult(this);
    }
}

class Woman extends People{

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

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

abstract class Action{
    //得到男的评价
    public abstract void getManResult(Man man);
    //得到女的评价
    public abstract void getWomanResult(Woman woman);
}

class Success extends Action{

    @Override
    public void getManResult(Man man) {
        System.out.println("男的赞同 "+ man.getName());
    }

    @Override
    public void getWomanResult(Woman woman) {
        System.out.println("女的赞同 " + woman.getName());
    }
}

class Fail extends Action{

    @Override
    public void getManResult(Man man) {
        System.out.println("男的反对" + man.getName());
    }

    @Override
    public void getWomanResult(Woman woman) {
        System.out.println("女的反对" + woman.getName());
    }
}

//数据结构,维护和管理很多人
class ObjectStructure{
    //维护集合
    private List<People> elements = new ArrayList<>();
    //添加一个人
    public void add(People p){
        elements.add(p);
    }
    //删除一个人
    public void remove(People p){
        elements.remove(p);
    }
    //显示测评情况
    public void display(Action action){
        for (People element : elements) {
            element.accept(action);
        }
    }
}

public class Client {
    public static void main(String[] args) {
        //创建两个评价标准
        Action success = new Success();
        Action fail = new Fail();
        //创建访问者对象
        Man bob = new Man("bob");
        Man tom = new Man("tom");
        Woman alice = new Woman("alice");
        //高层遍历访问者的对象
        ObjectStructure objectStructure = new ObjectStructure();
        //加入集合
        objectStructure.add(bob);
        objectStructure.add(tom);
        objectStructure.add(alice);
        //评价
        objectStructure.display(success);
        objectStructure.display(fail);

    }
}

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

  1. 小结

优点 :

  1. 访问者模式符合单一职责原则、让程序具有优秀的扩展性、灵活性非常高

  2. 访问者模式可以对功能进行统一,可以做报表、UI、拦截器与过滤器,适用于数据结构相对稳定的系统

缺点 :

  1. 具体元素对访问者公布细节,也就是说访问者关注了其他类的内部细节,这是迪米特法则所不建议的, 这样造成了具体元素变更比较困难

  2. 违背了依赖倒转原则。访问者依赖的是具体元素,而不是抽象元素 3) 因此,如果一个系统有比较稳定的数据结构,又有经常变化的功能需求,那么访问者模式就是比较合适的。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

攻城老湿

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值