zoukankan      html  css  js  c++  java
  • 设计模式学习笔记(十七):命令模式

    1 概述

    1.1 引言

    日常生活中,可以通过开关控制一些电器的开启和关闭,比如电灯和排气扇。可以将开关理解成一个请求发送者,电灯是请求的最红接收者以及处理者,开关与电灯之间不存在直接的耦合关系,两者通过电线连接在一起,使不同的电线可以连接不同的请求接收者,只需要更换一根电线,相同的发送者(开关)既可对应不同的接收者(电器)。

    软件开发中经常需要向某些对象发送请求,但是并不知道具体的接收者是谁,也不知道被请求的操作是哪个,此时希望以一种松耦合的方式来设计软件,使得请求发送者与请求接收者之间能够消除彼此之间的耦合,让对象之间的调用关系更加灵活,可以灵活地指定请求接收者以及被请求的操作,此时可以使用命令模式进行设计。

    命令模式可以将请求发送者和接收者完全解耦,发送者与接收者之间没有直接引用关系,发送请求的对象只需要知道如何发送请求,而不必知道如何完成请求。

    1.2 定义

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

    命令模式是一种对象行为型模式,别名为动作模式或者事务模式。

    1.3 结构图

    在这里插入图片描述

    1.4 角色

    • Command(抽象命令类):抽象命令类一般是一个抽象类或者接口,在其中声明了用于执行请求的execute()方法,通过这些方法可以调用请求接收者的相关操作
    • ConcreteCommand(具体命令类):实现了抽象命令类中声明的方法,对应具体的接收者对象,将接收者对象的动作绑定其中,在实现execute()方法时,将调用接收者对象的相关操作
    • Invoker(调用者):调用者即请求发送者,通过命令对象来执行请求。一个调用者并不需要设计时确定接收者,因此它只与抽象命令类之间存在关联关系。程序运行时将具体命令对象注入,并调用其中的execute()方法,从而实现间接调用请求接收者的相关操作
    • Receiver(接收者):接收者执行与请求相关的操作,具体实现对请求的业务处理

    2 典型实现

    2.1 步骤

    • 定义抽象命令类:定义执行请求的方法
    • 定义调用者:在调用方法里面包含对具体命令的调用,同时需要包含一个对抽象命令的引用
    • 定义接收者:定义接收请求的业务方法
    • 定义具体命令类:继承/实现抽象命令类,实现其中执行请求方法,转发到接收者的接收方法

    2.2 抽象命令类

    这里实现为一个接口:

    interface Command
    {
        void execute();
    }
    

    2.3 调用者

    class Invoker
    {
        private Command command;
    
        public Invoker(Command command)
        {
            this.command = command;
        }
    
        public void call()
        {
            System.out.println("调用者操作");
            command.execute();
        }
    }
    

    调用者可以通过构造方法或者setter注入具体命令,对外提供一个调用方法call,当调用此方法时调用具体命令的execute

    2.4 接收者

    class Receiver
    {
        public void action()
        {
            System.out.println("接收者操作");
        }
    }
    

    这里的接收者只有一个action,表示接收方法。

    2.5 具体命令类

    class ConcreteCommand implements Command
    {
        private Receiver receiver = new Receiver();
        @Override
        public void execute()
        {
            receiver.action();
        }
    }
    

    具体命令类中需要包含一个对接收者的引用,以便在execute中调用接收者。

    2.6 客户端

    public static void main(String[] args) 
    {
        Invoker invoker = new Invoker(new ConcreteCommand());
        invoker.call();
    }
    

    通过构造方法注入具体命令到调用者中,接着直接调用即可。

    输出如下:
    在这里插入图片描述

    3 实例

    自定义功能键的设置,对于一个按钮,可以根据需要由用户设置为最小化/最大化/关闭功能,使用命令模式进行设计。

    设计如下:

    • 抽象命令类:Command
    • 调用者:Button
    • 接收者:MinimizeHandler+MaximizeHandler+CloseHandler
    • 具体命令类:MinimizeCommand+MaximizeCommand+CloseCommand

    首先设计抽象命令类,实现为一个接口,仅包含execute方法:

    interface Command
    {
        void execute();
    }
    

    接着是调用者类,包含一个抽象命令的引用:

    class Button
    {
        private Command command;
        public Button(Command command)
        {
            this.command = command;
        }
    
        public void onClick()
        {
            System.out.println("按钮被点击");
            command.execute();
        }
    }
    

    然后是接收者类:

    class MinimizeHandler
    {
        public void handle()
        {
            System.out.println("最小化");
        }
    }
    
    class MaximizeHandler
    {
        public void handle()
        {
            System.out.println("最大化");
        }
    }
    
    class CloseHandler
    {
        public void handle()
        {
            System.out.println("关闭");
        }
    }
    

    最后是具体命令类,对应包含一个接收者成员即可,实现其中的execute并转发到接收者的方法:

    class MinimizeCommand implements Command
    {
        private MinimizeHandler handler = new MinimizeHandler();
        @Override
        public void execute()
        {
            handler.handle();
        }
    }
    
    class MaximizeCommand implements Command
    {
        private MaximizeHandler handler = new MaximizeHandler();
        @Override
        public void execute()
        {
            handler.handle();
        }
    }
    
    class CloseCommand implements Command
    {
        private CloseHandler handler = new CloseHandler();
        @Override
        public void execute()
        {
            handler.handle();
        }
    }
    

    测试类:

    public static void main(String[] args) 
    {
        Button button = new Button(new MinimizeCommand());
        button.onClick();
    
        button = new Button(new MaximizeCommand());
        button.onClick();
    
        button = new Button(new CloseCommand());
        button.onClick();
    }
    

    输出:
    在这里插入图片描述
    如果需要新增一个命令,只需要命令接收者以及实现了Command的具体命令类,客户端再将具体命令注入请求发送者(Button),无须直接操作请求接收者。

    4 命令队列

    有时候需要将多个请求排队,当一个请求发送者发送完成一个请求后,不止一个请求接收者产生响应,这些请求接收者将逐个执行业务方法完成对请求的处理。这种形式可以通过命令队列实现,实现命令队列很简单,一般是增加一个叫CommandQueue的类,由该类负责存储多个命令对象,不同的命令对象可以对应不同的请求接收者,比如在上面的例子中增加CommandQueue命令队列类:

    class CommandQueue
    {
        private ArrayList<Command> commands = new ArrayList<>();
        public void add(Command command)
        {
            commands.add(command);
        }
    
        public void remove(Command command)
        {
            commands.remove(command);
        }
    
        public void execute()
        {
            System.out.println("批量执行命令");
            commands.forEach(Command::execute);
        }
    }
    

    接着修改调用者类Button(只需将原来的Command改为CommandQueue):

    class Button
    {
        private CommandQueue queue;
        public Button(CommandQueue queue)
        {
            this.queue = queue;
        }
    
        public void onClick()
        {
            System.out.println("按钮被点击");
            queue.execute();
        }
    }
    

    最后是客户端定义命令队列并作为参数传入调用者的构造方法或者setter中,最后由调用者执行方法:

    public static void main(String[] args) 
    {
        CommandQueue queue = new CommandQueue();
        queue.add(new MinimizeCommand());
        queue.add(new MaximizeCommand());
        queue.add(new CloseCommand());
        Button button = new Button(queue);
        button.onClick();
    }
    

    输出如下:
    在这里插入图片描述

    5 撤销与重做

    设计一个简易计算器,实现加法功能,还能够实现撤销以及重做功能,使用命令模式实现。

    设计如下:

    • 抽象命令类:Command
    • 调用者:Calculator
    • 接收者:Adder
    • 具体命令类:AddCommand

    首先先不实现撤销以及重做功能:

    public class Test
    {
        public static void main(String[] args) 
        {
            Calculator calculator = new Calculator(new AddCommand());
            calculator.add(3);
            calculator.add(9);
        }
    }
    
    interface Command
    {
        int execute(int value);
    }
    
    class Calculator
    {
        private Command command;
        public Calculator(Command command)
        {
            this.command = command;
        }
    
        public void add(int value)
        {
            System.out.println(command.execute(value));
        }
    }
    
    class Adder
    {
        private int num = 0;
        public int add(int value)
        {
            return num += value;
        }
    }
    
    class AddCommand implements Command
    {
        private Adder adder = new Adder();
        @Override
        public int execute(int value)
        {
            return adder.add(value);
        }
    }
    

    代码与上面的实例类似,就不解释了。

    这里关键的问题是如何实现撤销以及重做功能,撤销能够恢复到进行加法之前的状态,而重做能恢复到进行了加法之后的状态,而且这是有固定顺序的,因此可以联想到数组,使用下标表示当前状态,下标左移表示撤销,下标右移表示重做:
    在这里插入图片描述
    使用一个状态数组存储每次进行加法的状态,用下标表示当前状态,当撤销时,使下标左移,当重做时,使下标右移。

    首先需要修改抽象命令类,添加撤销以及重做方法:

    interface Command
    {
        int execute(int value);
        int undo();
        int redo();
    }
    

    接着修改调用者类,添加撤销以及重做方法:

    class Calculator
    {
        private Command command;
        public Calculator(Command command)
        {
            this.command = command;
        }
    
        public void add(int value)
        {
            System.out.println(command.execute(value));
        }
    
        public void undo()
        {
            System.out.println(command.undo());
        }
    
        public void redo()
        {
            System.out.println(command.redo());
        }
    }
    

    核心的实现位于接收者类Adder,使用了List<Integer>存储了状态,index表示下标,在撤销或重做之前首先判断下标位置是否合法,合法则进行下一步操作:

    class Adder
    {
        private List<Integer> nums = new ArrayList<>();
        private int index = 0;
        public Adder()
        {
            nums.add(0);
        }
    
        public int add(int value)
        {
            int result = nums.get(index)+value;
            nums.add(result);
            ++index;
            return result;
        }
    
        public int redo()
        {
            if(index + 1 < nums.size())
                return nums.get(++index);
            return nums.get(index);
        }
    
        public int undo()
        {
            if(index - 1 >= 0)
                return nums.get(--index);
            return nums.get(index);
        }
    }
    

    最后具体命令类简单添加撤销以及重做方法即可:

    class AddCommand implements Command
    {
        private Adder adder = new Adder();
        @Override
        public int execute(int value)
        {
            return adder.add(value);
        }
    
        @Override
        public int undo()
        {
            return adder.undo();
        }
    
        @Override
        public int redo()
        {
            return adder.redo();
        }
    }
    

    测试:

    public static void main(String[] args) 
    {
        Calculator calculator = new Calculator(new AddCommand());
        calculator.add(3);
        calculator.add(9);
        
        calculator.undo();
        calculator.undo();
        calculator.undo();
        calculator.undo();
        
        calculator.redo();
        calculator.redo();
        calculator.redo();
        calculator.redo();
    }
    

    在这里插入图片描述

    6 主要优点

    • 降低耦合度:由于请求者与接收者之间不存在直接引用,因此请求者与接收者之间实现完全解耦,相同的请求可以对应不同的接收者,同样相同的接收者也可以供不同的请求者使用,两者之间具有良好的独立性
    • 满足OCP:新的命令可以很容易添加到系统中,由于增加新的具体命令类不会影响到其他类,因此增加新的具体命令类很容易,满足OCP的要求
    • 撤销+中作:为请求的撤销以及重做提供了一种设计和实现方案

    7 主要缺点

    • 过多具体命令类:使用命令模式可能会导致系统有过多的具体命令类,因为针对每一个请求接收者的调用操作都需要设计一个具体工具类,因此在某些系统中可能需要提供大量的具体命令类

    8 适用场景

    • 系统需要将请求调用者和请求接受者解耦,使得调用者和接收者不直接交互,请求调用者无须知道接收者的存在,也无需知道接收者是谁,接收者也无须关心何时被调用
    • 系统需要在不同时间指定请求,将请求排队和执行请求
    • 系统需要支持撤销以及恢复操作
    • 系统需要将一组操作组合一起形成宏命令,使用命令队列实现

    9 总结

    在这里插入图片描述

    如果觉得文章好看,欢迎点赞。

    同时欢迎关注微信公众号:氷泠之路。

    在这里插入图片描述

  • 相关阅读:
    csrf攻击实例
    《四 数据库连接池源码》手写数据库连接池
    《四 spring源码》手写springmvc
    spring和springmvc是单例还是多例
    redis集群设置密码
    mongodb3.6集群搭建:分片集群认证
    mongodb3.6集群搭建:分片+副本集
    Mongo 3.6.1版本Sharding集群配置
    windows计划任务
    Redis slowlog慢查询
  • 原文地址:https://www.cnblogs.com/6b7b5fc3/p/13417638.html
Copyright © 2011-2022 走看看