设计模式十三——命令模式

tech2022-09-26  91

1.概述

1.1引言

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

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

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

1.2命令模式定义

将一个请求封装为一个对象,从而使你可用不同的请求对客户端参数化;对请求排队或记录请求日志,以及支持可撤销的操作. 简单的说就是将请求封装成一个对象,在这里就是这个对象就是命令,而这个命令就是将请求方和执行方分离隔开。流程就是请求方发出请求要求执行某操作,接收方收到请求后并执行对应的操作。

1.3类图及角色

结构类图: 角色说明:

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

2.典型实现

步骤:

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

实现:

抽象命令类

interface ICommand { void execute(); }

调用者

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

接收者

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

具体命令类

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

测试:

public class Client { 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(); }

调用者

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

接收者类

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

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

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

测试类

public class Client { 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.命令模式优缺点及适用场景

优点:

(1) 降低系统的耦合度。由于请求者与接收者之间不存在直接引用,因此请求者与接收者之间实现完全解耦,相同的请求者可以对应不同的接收者,同样,相同的接收者也可以供不同的请求者使用,两者之间具有良好的独立性。

(2) 新的命令可以很容易地加入到系统中。由于增加新的具体命令类不会影响到其他类,因此增加新的具体命令类很容易,无须修改原有系统源代码,甚至客户类代码,满足“开闭原则”的要求。

(3) 可以比较容易地设计一个命令队列或宏命令(组合命令)。

(4) 为请求的撤销(Undo)和恢复(Redo)操作提供了一种设计和实现方案。

缺点:

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

适用场景:

(1) 系统需要将请求调用者和请求接收者解耦,使得调用者和接收者不直接交互。请求调用者无须知道接收者的存在,也无须知道接收者是谁,接收者也无须关心何时被调用。

(2) 系统需要在不同的时间指定请求、将请求排队和执行请求。一个命令对象和请求的初始调用者可以有不同的生命期,换言之,最初的请求发出者可能已经不在了,而命令对象本身仍然是活动的,可以通过该命令对象去调用请求接收者,而无须关心请求调用者的存在性,可以通过请求日志文件等机制来具体实现。

(3) 系统需要支持命令的撤销(Undo)操作和恢复(Redo)操作。

(4) 系统需要将一组操作组合在一起形成宏命令。

(5)线程池有一个addTash方法,将任务添加到待完成的队列中,队列中的元素就是命令对象,通常的就是一个公共接口,像我们常用的java.lang.Runnable接口。

(6)java8之后,最好在Command接口中@FunctionalInterface修饰,这样具体的命令就可以使用lambda表达式啦。

引用: 引用文章地址:命令模式https://juejin.im/post/6856174541518389261#heading-13

最新回复(0)