您现在的位置是:首页 >技术教程 >设计模式之【命令模式】,方法调用的花式玩法网站首页技术教程
设计模式之【命令模式】,方法调用的花式玩法
文章目录
一、什么是命令模式
命令模式(Command Pattern)是对命令的封装,每一个命令都是一个操作:请求的一方发出请求要求执行一个操作;接收的一方收到请求,并执行操作。命令模式解耦了请求方和接收方,请求方只需请求执行命令,不用关心命令是怎样被接受,怎样被操作以及是否被执行等。命令模式属于行为型模式。
原文:将一个请求封装成医德对象,从而让你使用不同的请求把客户端参数化,对请求排队或者记录请求日志,可以提供命令的撤销和恢复功能。
在软件系统中,行为请求者与行为实现者通常是一种紧耦合关系,因为这样的实现简单明了。但紧耦合关系缺乏扩展性,在某些场合中,当需要为行为进行记录,撤销或重做等处理时,只能修改源码。而命令模式通过为请求与实现间引入了一个抽象命令接口,解耦了请求与实现,并且中间件是抽象的,它可以有不同的子类实现,因此其具备扩展性。所以,命令模式的本质是解耦命令请求与处理。
1、命令模式使用场景
当系统的某项操作具备命令语义时,且命令实现不稳定(变化),那么可以通过命令模式解耦请求与实现,利用抽象命令接口使请求方代码架构稳定,封装接收方具体命令实现细节。接收方与抽象命令接口呈现弱耦合(内部方法无需一致),具备良好的扩展性。命令模式适用于以下应用场景:
- 系统需要将请求调用者和请求接收者解耦,使得调用者和接收者不直接交互。
- 现实语义中具备“命令”的操作(如命令菜单、shell命令等)。
- 系统需要支持命令的撤销(Undo)操作和恢复(Redo)操作。
- 系统需要在不同的时间指定请求、将请求排队和执行请求。
- 需要支持命令宏(即命令组合操作)。
2、命令模式的主要角色
命令模式包含以下主要角色:
- 抽象命令类(Command)角色: 定义命令的接口,声明执行的方法。
- 具体命令(Concrete Command)角色:具体的命令,实现命令接口;通常会持有接收者,并调用接收者的功能来完成命令要执行的操作。
- 实现者/接收者(Receiver)角色: 接收者,真正执行命令的对象。任何类都可能成为一个接收者,只要它能够实现命令要求实现的相应功能。
- 调用者/请求者(Invoker)角色: 要求命令对象执行请求,通常会持有命令对象,可以持有很多的命令对象。这个是客户端真正触发命令并要求命令执行相应操作的地方,也就是说相当于使用命令对象的入口。
从命令模式的UML类图中可以看出:Command的出现就是作为Receiver和Invoker的中间件,解耦了彼此。而之所以引入Command中间件,主要有以下两方面的原因:
(1)解耦请求与实现:即解耦了Invoker和Receiver,因为在UML类图中,Invoker是一个具体的实现,等待接收客户端传入命令(即Invoker与客户端耦合),Invoker处于业务逻辑区域,应当是一个稳定的结构。而Receiver是属于业务功能模块,是经常变动的;如果没有Command,则Invoker紧耦合Receiver,一个稳定的结构依赖了一个不稳定的结构,就会导致整个结构都不稳定了。这也就是Command引入的原因:不仅仅是解耦请求与实现,同时稳定(Invoker)依赖文档(Command),结构还是稳定的。
(2)扩展性增强:扩展性体现在两个方面:① Receiver属于底层细节,可以通过更换不同的Receiver达到不同的细节实现;② Command接口本身就是抽象的,本身就具备扩展性,而且由于命令对象本身就具备抽象,如果结合装饰器模式,功能扩展简直如鱼得水。
注!在一个系统中,不同的命令对应不同的请求,也就是说无法把请求抽象化,因此命令模式中的Receiver是具体实现;但是如果在某一个模块中,可以对Receiver进行抽象,其实这就变相使用到了桥接模式(Command类具备两个变化的维度:Command和Receiver),这样子的扩展性会更加优秀。
3、命令模式优缺点
优点:
- 降低系统的耦合度。命令模式能将调用操作的对象与实现该操作的对象解耦。
- 增加或删除命令非常方便。采用命令模式增加与删除命令不会影响其他类,它满足“开闭原则”,对扩展比较灵活。
- 可以实现宏命令。命令模式可以与组合模式结合,将多个命令装配成一个组合命令,即宏命令。
- 方便实现 Undo 和 Redo 操作。命令模式可以与备忘录模式结合,实现命令的撤销与恢复。
- 可以在现有命令的基础上,增加额外功能(比如日志记录等等,结合装饰器模式效果更佳)。
缺点:
- 使用命令模式可能会导致某些系统有过多的具体命令类。
- 命令模式的结果其实就是接收方的执行结果,但是为了以命令的形式进行架构,解耦请求与实现,引入了额外类型结构(引入了请求方与抽象命令接口),增加了理解上的困难(不过这也是设计模式带来的一个通病,抽象必然会引入额外类型;抽象肯定比紧密难理解)。
- 使用频率低、理解难度大,只在非常特定的应用场景下才会用到。
4、命令模式注意事项及细节
- 将发起请求的对象与执行请求的对象解耦。发起请求的对象是调用者,调用者只要调用命令对象execute()方法就可以让接收者工作,而不必知道具体的接收者对象是谁、是如何实现的,命令对象会负责让接收者执行请求的动作,也就是说:“请求发起者”和“请求执行者”之间的解耦是通过命令对象实现的,命令对象起到了纽带桥梁的作用。
- 容易设计一个命令队列,只要把命令对象放到队列,就可以多线程的执行命令。
- 容易实现对请求的撤销和重做。
空命令也是一种设计模式
,它为我们省去了判空的操作。可以使用接口适配器模式,将Command接口适配为一个空类。
二、使用示例
1、命令模式的一般写法
//抽象命令接口
public interface ICommand {
void execute();
}
//具体命令
public class ConcreteCommand implements ICommand {
// 直接创建接收者,不暴露给客户端
private Receiver mReceiver;
public ConcreteCommand(Receiver mReceiver) {
this.mReceiver = mReceiver;
}
public void execute() {
this.mReceiver.action();
}
}
//请求者
public class Invoker {
private ICommand mCmd;
public Invoker(ICommand cmd) {
this.mCmd = cmd;
}
public void action() {
this.mCmd.execute();
}
}
//接收者
public class Receiver {
public void action() {
System.out.println("执行具体操作");
}
}
public class Test {
public static void main(String[] args) {
ICommand cmd = new ConcreteCommand(new Receiver());
Invoker invoker = new Invoker(cmd);
invoker.action();
}
}
2、播放器功能案例
假如我们自己开发一个播放器,播放器有播放功能、拖拽进度条功能、停止播放功能、暂停播放功能,我们自己去操作播放器的时候并不是直接调用播放器的方法,而是通过一个控制条去传达指令给播放器内核,那么具体传达什么指令,会被封装为一个个的按钮。那么每个按钮就相当于是对一条命令的封装。用控制条实现了用户发送指令与播放器内核接收指令的解耦。
// Receiver角色:播放器内核GPlayer
public class GPlayer {
public void play(){
System.out.println("正常播放");
}
public void speed(){
System.out.println("拖动进度条");
}
public void stop(){
System.out.println("停止播放");
}
public void pause(){
System.out.println("暂停播放");
}
}
// 抽象命令Command角色:命令接口
public interface IAction {
void execute();
}
// 具体命令角色
public class StopAction implements IAction {
private GPlayer gplayer;
public StopAction(GPlayer gplayer) {
this.gplayer = gplayer;
}
public void execute() {
gplayer.stop();
}
}
public class PauseAction implements IAction {
private GPlayer gplayer;
public PauseAction(GPlayer gplayer) {
this.gplayer = gplayer;
}
public void execute() {
gplayer.pause();
}
}
public class PlayAction implements IAction {
private GPlayer gplayer;
public PlayAction(GPlayer gplayer) {
this.gplayer = gplayer;
}
public void execute() {
gplayer.play();
}
}
public class SpeedAction implements IAction {
private GPlayer gplayer;
public SpeedAction(GPlayer gplayer) {
this.gplayer = gplayer;
}
public void execute() {
gplayer.speed();
}
}
// Invoker 角色
public class Controller {
private List<IAction> actions = new ArrayList<IAction>();
public void addAction(IAction action){
actions.add(action);
}
public void execute(IAction action){
action.execute();
}
public void executes(){
for (IAction action:actions) {
action.execute();
}
actions.clear();
}
}
public class Test {
public static void main(String[] args) {
GPlayer player = new GPlayer();
Controller controller = new Controller();
controller.execute(new PlayAction(player));
controller.addAction(new PauseAction(player));
controller.addAction(new PlayAction(player));
controller.addAction(new StopAction(player));
controller.addAction(new SpeedAction(player));
controller.executes();
}
}
3、遥控器案例
买了一套智能家电,有点灯、风扇、冰箱、洗衣机、电视机,我们只需要在手机上安装app就可以控制这些家电。
//创建命令接口
public interface Command {
//执行动作(操作)
public void execute();
//撤销动作(操作)
public void undo();
}
/**
* 没有任何命令,即空执行: 用于初始化每个按钮, 当调用空命令时,对象什么都不做
* 其实,这样是一种设计模式, 可以省掉对空判断
*/
public class NoCommand implements Command {
@Override
public void execute() {
}
@Override
public void undo() {
}
}
// 电灯Receiver
public class LightReceiver {
public void on() {
System.out.println(" 电灯打开了.. ");
}
public void off() {
System.out.println(" 电灯关闭了.. ");
}
}
// 电灯具体命令类
public class LightOffCommand implements Command {
// 聚合LightReceiver
LightReceiver light;
// 构造器
public LightOffCommand(LightReceiver light) {
super();
this.light = light;
}
@Override
public void execute() {
// 调用接收者的方法
light.off();
}
@Override
public void undo() {
// 调用接收者的方法
light.on();
}
}
// 电灯具体命令类
public class LightOnCommand implements Command {
//聚合LightReceiver
LightReceiver light;
//构造器
public LightOnCommand(LightReceiver light) {
super();
this.light = light;
}
@Override
public void execute() {
//调用接收者的方法
light.on();
}
@Override
public void undo() {
//调用接收者的方法
light.off();
}
}
// 电视机Receiver
public class TVReceiver {
public void on() {
System.out.println(" 电视机打开了.. ");
}
public void off() {
System.out.println(" 电视机关闭了.. ");
}
}
// 电视机具体命令
public class TVOnCommand implements Command {
// 聚合TVReceiver
TVReceiver tv;
// 构造器
public TVOnCommand(TVReceiver tv) {
super();
this.tv = tv;
}
@Override
public void execute() {
// 调用接收者的方法
tv.on();
}
@Override
public void undo() {
// 调用接收者的方法
tv.off();
}
}
public class TVOffCommand implements Command {
// 聚合TVReceiver
TVReceiver tv;
// 构造器
public TVOffCommand(TVReceiver tv) {
super();
this.tv = tv;
}
@Override
public void execute() {
// 调用接收者的方法
tv.off();
}
@Override
public void undo() {
// 调用接收者的方法
tv.on();
}
}
// 控制器,Invoker角色
public class RemoteController {
// 开 按钮的命令数组
Command[] onCommands;
Command[] offCommands;
// 执行撤销的命令
Command undoCommand;
// 构造器,完成对按钮初始化
public RemoteController() {
onCommands = new Command[5];
offCommands = new Command[5];
for (int i = 0; i < 5; i++) {
onCommands[i] = new NoCommand();
offCommands[i] = new NoCommand();
}
}
// 给我们的按钮设置你需要的命令
public void setCommand(int no, Command onCommand, Command offCommand) {
onCommands[no] = onCommand;
offCommands[no] = offCommand;
}
// 按下开按钮
public void onButtonWasPushed(int no) { // no 0
// 找到你按下的开的按钮, 并调用对应方法
onCommands[no].execute();
// 记录这次的操作,用于撤销
undoCommand = onCommands[no];
}
// 按下开按钮
public void offButtonWasPushed(int no) { // no 0
// 找到你按下的关的按钮, 并调用对应方法
offCommands[no].execute();
// 记录这次的操作,用于撤销
undoCommand = offCommands[no];
}
// 按下撤销按钮
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();
//给我们的遥控器设置命令, 比如 no = 0 是电灯的开和关的操作
remoteController.setCommand(0, lightOnCommand, lightOffCommand);
System.out.println("--------按下灯的开按钮-----------");
remoteController.onButtonWasPushed(0);
System.out.println("--------按下灯的关按钮-----------");
remoteController.offButtonWasPushed(0);
System.out.println("--------按下撤销按钮-----------");
remoteController.undoButtonWasPushed();
System.out.println("=========使用遥控器操作电视机==========");
TVReceiver tvReceiver = new TVReceiver();
TVOffCommand tvOffCommand = new TVOffCommand(tvReceiver);
TVOnCommand tvOnCommand = new TVOnCommand(tvReceiver);
//给我们的遥控器设置命令, 比如 no = 1 是电视机的开和关的操作
remoteController.setCommand(1, tvOnCommand, tvOffCommand);
System.out.println("--------按下电视机的开按钮-----------");
remoteController.onButtonWasPushed(1);
System.out.println("--------按下电视机的关按钮-----------");
remoteController.offButtonWasPushed(1);
System.out.println("--------按下撤销按钮-----------");
remoteController.undoButtonWasPushed();
}
}
三、源码中的命令模式
1、Thread
Runable是一个典型命令模式,Runnable担当命令的角色,Thread充当的是调用者,start方法就是其执行方法。
//命令接口(抽象命令角色)
public interface Runnable {
public abstract void run();
}
//调用者
public class Thread implements Runnable {
private Runnable target;
public synchronized void start() {
if (threadStatus != 0)
throw new IllegalThreadStateException();
group.add(this);
boolean started = false;
try {
start0();
started = true;
} finally {
try {
if (!started) {
group.threadStartFailed(this);
}
} catch (Throwable ignore) {
}
}
}
private native void start0();
}
会调用一个native方法start0(),调用系统方法,开启一个线程。而接收者是对程序员开放的,可以自己定义接收者。
/**
* jdk Runnable 命令模式
* TurnOffThread : 属于具体
*/
public class TurnOffThread implements Runnable{
private Receiver receiver;
public TurnOffThread(Receiver receiver) {
this.receiver = receiver;
}
public void run() {
receiver.turnOFF();
}
}
/**
* 测试类
*/
public class Demo {
public static void main(String[] args) {
Receiver receiver = new Receiver();
TurnOffThread turnOffThread = new TurnOffThread(receiver);
Thread thread = new Thread(turnOffThread);
thread.start();
}
}
实际上调用线程的start方法之后,就有资格去抢CPU资源,而不需要我们自己编写获得CPU资源的逻辑。而线程抢到CPU资源后,就会执行run方法中的内容,用Runnable接口把用户请求和CPU执行进行了解耦。