命令模式与访问者模式

命令模式与访问者模式

参考教程: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) 因此,如果一个系统有比较稳定的数据结构,又有经常变化的功能需求,那么访问者模式就是比较合适的。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 199,830评论 5 468
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 83,992评论 2 376
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 146,875评论 0 331
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,837评论 1 271
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,734评论 5 360
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,091评论 1 277
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,550评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,217评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,368评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,298评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,350评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,027评论 3 315
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,623评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,706评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,940评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,349评论 2 346
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,936评论 2 341