设计模式 ~ 结构型模式 ~ 命令模式 ~ Command Pattern。
概述。
定义。
将一个请求封装为一个对象,使发出请求的责任和执行请求的责任分割开。这样两者之间通过命令对象进行沟通,这样方便将命令对象进行存储、传递、调用、增加与管理。
结构。
命令模式包含以下主要角色。
-
抽象命令类(Command)角色。
定义命令的接口,声明执行的方法。 -
具体命令(Concrete Command)角色。
具体的命令,实现命令接口;通常会持有接收者,并调用接收者的功能来完成命令要执行的操作。 -
实现者/接收者(Receiver)角色。
接收者,真正执行命令的对象。任何类都可能成为一个接收者,只要它能够实现命令要求实现的相应功能。 -
调用者/请求者(Invoker)角色。
要求命令对象执行请求,通常会持有命令对象,可以持有很多的命令对象。这个是客户端真正触发命令并要求命令执行相应操作的地方,也就是说相当于使用命令对象的入口。
案例实现。
将上面的案例用代码实现,那我们就需要分析命令模式的角色在该案例中由谁来充当。
服务员: 就是调用者角色,由她来发起命令。
资深大厨: 就是接收者角色,真正命令执行的对象。
订单: 命令中包含订单。
类图如下。
代码如下。
package com.geek.command.pattern;
import lombok.Data;
import java.util.HashMap;
import java.util.Map;
/**
* 订单类。
*
* @author geek
*/
@Data
public class Order {
/**
* 餐桌号码。
*/
private Integer diningTable;
/**
* 餐品及份数。
*/
private Map<String, Integer> foodMap = new HashMap<String, Integer>();
}
package com.geek.command.pattern;
/**
* 资深大厨类 ~ 命令的 Receiver。
*
* @author geek
*/
public class SeniorChef {
public void makeFood(String foodName, Integer num) {
System.out.println(num + " 份" + foodName);
}
}
package com.geek.command.pattern;
/**
* 接口 ~ 抽象命令。
*
* @author geek
*/
public interface ICommand {
/**
* 只需要定义一个统一的执行方法。
*/
void execute();
}
package com.geek.command.pattern;
import lombok.AllArgsConstructor;
import lombok.Data;
import java.util.Set;
/**
* 具体的命令类。
*
* @author geek
*/
@Data
@AllArgsConstructor
public class OrderCommand implements ICommand {
/**
* 持有接受者对象。
*/
private SeniorChef receiver;
private Order order;
public void execute() {
System.out.println(this.order.getDiningTable() + " 桌的订单:");
Set<String> keys = this.order.getFoodMap().keySet();
for (String key : keys) {
this.receiver.makeFood(key, this.order.getFoodMap().get(key));
}
try {
// 停顿一下,模拟做饭的过程。
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(this.order.getDiningTable() + " 桌的饭准备好了。");
}
}
package com.geek.command.pattern;
import java.util.ArrayList;
import java.util.List;
/**
* 服务员类 ~ 请求者角色。
*
* @author geek
*/
public class Waiter {
/**
* 可以持有很多的命令对象。
*/
private final List<ICommand> commandList;
public Waiter() {
commandList = new ArrayList<>();
}
public void setCommand(ICommand cmd) {
// 将 command 对象存储到 list 集合中。
this.commandList.add(cmd);
}
/**
* 发出命令 喊 订单来了,厨师开始执行。
*/
public void orderUp() {
System.out.println("美女服务员:叮咚,大厨,新订单来了。");
for (ICommand cmd : this.commandList) {
if (cmd != null) {
cmd.execute();
}
}
}
}
package com.geek.command.pattern;
/**
* @author geek
*/
public class Client {
public static void main(String[] args) {
// 创建 2 个 order。
Order order1 = new Order();
order1.setDiningTable(1);
order1.getFoodMap().put("西红柿鸡蛋面", 1);
order1.getFoodMap().put("小杯可乐", 2);
Order order2 = new Order();
order2.setDiningTable(3);
order2.getFoodMap().put("尖椒肉丝盖饭", 1);
order2.getFoodMap().put("小杯雪碧", 1);
// 创建接收者。
SeniorChef receiver = new SeniorChef();
// 将订单和接收者封装成命令对象。
OrderCommand cmd1 = new OrderCommand(receiver, order1);
OrderCommand cmd2 = new OrderCommand(receiver, order2);
// 创建调用者 waiter。
Waiter invoker = new Waiter();
invoker.setCommand(cmd1);
invoker.setCommand(cmd2);
// 将订单带到柜台,并向厨师喊:订单来了。
invoker.orderUp();
}
}
/*
Connected to the target VM, address: '127.0.0.1:65004', transport: 'socket'
美女服务员:叮咚,大厨,新订单来了。
1 桌的订单:
1 份西红柿鸡蛋面
2 份小杯可乐
1 桌的饭准备好了。
3 桌的订单:
1 份尖椒肉丝盖饭
1 份小杯雪碧
3 桌的饭准备好了。
Disconnected from the target VM, address: '127.0.0.1:65004', transport: 'socket'
Process finished with exit code 0
*/
优缺点。
优点。
-
降低系统的耦合度。命令模式能将调用操作的对象与实现该操作的对象解耦。
-
增加或删除命令非常方便。采用命令模式增加与删除命令不会影响其他类,它满足“开闭原则”,对扩展比较灵活。
-
可以实现宏命令。命令模式可以与组合模式结合,将多个命令装配成一个组合命令,即宏命令。
-
方便实现 Undo 和 Redo 操作。命令模式可以与后面介绍的备忘录模式结合,实现命令的撤销与恢复。
缺点。
-
使用命令模式可能会导致某些系统有过多的具体命令类。
-
系统结构更加复杂。
使用场景。
-
系统需要将请求调用者和请求接收者解耦,使得调用者和接收者不直接交互。
-
系统需要在不同的时间指定请求、将请求排队和执行请求。
-
系统需要支持命令的撤销(Undo)操作和恢复(Redo)操作。
JDK 源码解析。
Runable 是一个典型命令模式,Runnable 担当命令的角色,Thread 充当的是调用者,start 方法就是其执行方法。
Runnable // 命令接口,抽象命令角色。
/*
* Copyright (c) 1994, 2013, Oracle and/or its affiliates. All rights reserved.
* ORACLE PROPRIETARY/CONFIDENTIAL. Use is subject to license terms.
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*/
package java.lang;
/**
* The <code>Runnable</code> interface should be implemented by any
* class whose instances are intended to be executed by a thread. The
* class must define a method of no arguments called <code>run</code>.
* <p>
* This interface is designed to provide a common protocol for objects that
* wish to execute code while they are active. For example,
* <code>Runnable</code> is implemented by class <code>Thread</code>.
* Being active simply means that a thread has been started and has not
* yet been stopped.
* <p>
* In addition, <code>Runnable</code> provides the means for a class to be
* active while not subclassing <code>Thread</code>. A class that implements
* <code>Runnable</code> can run without subclassing <code>Thread</code>
* by instantiating a <code>Thread</code> instance and passing itself in
* as the target. In most cases, the <code>Runnable</code> interface should
* be used if you are only planning to override the <code>run()</code>
* method and no other <code>Thread</code> methods.
* This is important because classes should not be subclassed
* unless the programmer intends on modifying or enhancing the fundamental
* behavior of the class.
*
* @author Arthur van Hoff
* @see java.lang.Thread
* @see java.util.concurrent.Callable
* @since JDK1.0
*/
@FunctionalInterface
public interface Runnable {
/**
* When an object implementing interface <code>Runnable</code> is used
* to create a thread, starting the thread causes the object's
* <code>run</code> method to be called in that separately executing
* thread.
* <p>
* The general contract of the method <code>run</code> is that it may
* take any action whatsoever.
*
* @see java.lang.Thread#run()
*/
public abstract void run();
}
//调用者。
public
class Thread implements Runnable {
...
/* What will be run. */
private Runnable target;
...
public synchronized void start() {
/**
* This method is not invoked for the main method thread or "system"
* group threads created/set up by the VM. Any new functionality added
* to this method in the future may have to also be added to the VM.
*
* A zero status value corresponds to state "NEW".
*/
if (threadStatus != 0)
throw new IllegalThreadStateException();
/* Notify the group that this thread is about to be started
* so that it can be added to the group's list of threads
* and the group's unstarted count can be decremented. */
group.add(this);
boolean started = false;
try {
start0();
started = true;
} finally {
try {
if (!started) {
group.threadStartFailed(this);
}
} catch (Throwable ignore) {
/* do nothing. If start0 threw a Throwable then
it will be passed up the call stack */
}
}
}
private native void start0();
。。。
}
会调用一个 native 方法 start0(); 调用系统方法,开启一个线程。而接收者是对程序员开放的,可以自己定义接收者。
package com.geek.command.pattern.demo;
/**
* jdk Runnable 命令模式。
* TurnOffThread ~ 属于具体命令角色。
*
* @author geek
*/
public class TurnOffThread implements Runnable {
private final Receiver receiver;
public TurnOffThread(Receiver receiver) {
this.receiver = receiver;
}
public void run() {
this.receiver.turnOff();
}
}
package com.geek.command.pattern.demo;
/**
* @author geek
*/
public class Receiver {
public void turnOff() {
System.out.println("turnOff.");
}
}
package com.geek.command.pattern.demo;
/**
* 测试类。
*
* @author geek
*/
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();
}
}