设计模式学习笔记(十七):命令模式

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 总结

在这里插入图片描述

posted @ 2020-08-02 08:34  氷泠  阅读(257)  评论(0编辑  收藏  举报