命令模式
定义:
《设计模式》中命令模式的定义为:将一个请求封装为一个对象,从而使你可用不同的请求对客户进行参数化;对请求排队或记录请求日志,以及支持可撤消的操作。
命令模式Command 别名:
- Action----动作模式
- Transaction ----事务模式
为什么需要命令模式?
在我们的软件开发系统中,行为请求者和真正的执行者通常都是一种紧耦合关系,但是这种情况下,当我们需要修改行为时,如需要撤销或者重做时,只能修改请求者的源代码,
- 命令模式会通过在行为请求者和执行者之间引入一个抽象接口来将请求者和执行者进行解耦,这样如果需要修改行为时,只需要增加对应行为的命令就可以了,完全不需要修改请求者的源代码。
- 通过在你的请求者和真正的执行者之间加上了一个中间人的角色,来达到分离耦合的目的。通过对中间人角色的特殊设计来形成不同的模式。
命令模式的目的就是达到命令的发出者和执行者之间解耦,实现请求和执行分开
组成:
案例 1
需求:
我们对最普通的文本文字,做一系列处理命令
- 包括复制文本内容,插入指定文本内容,删除指定长度文字内容
- 并且可以撤销以上操作,实现回滚.
接受者角色 Content 文本内容
public class Content {
String msg = "hello everybody ";
}
抽象命令角色: Command
public abstract class Command {
public abstract void doit(); //exec run
public abstract void undo();
}
实际复制命令角色: CopyCommand
public class CopyCommand extends Command {
Content c;
public CopyCommand(Content c) {
this.c = c;
}
@Override
public void doit() {
c.msg = c.msg + c.msg;
}
@Override
public void undo() {
c.msg = c.msg.substring(0, c.msg.length()/2);
}
}
实际删除命令角色: DeleteCommand
public class DeleteCommand extends Command {
Content c;
String deleted;
public DeleteCommand(Content c) {
this.c = c;
}
@Override
public void doit() {
deleted = c.msg.substring(0, 5);
c.msg = c.msg.substring(5, c.msg.length());
}
@Override
public void undo() {
c.msg = deleted + c.msg;
}
}
实际插入命令角色: InsertCommand
public class InsertCommand extends Command {
Content c;
String strToInsert = "http://www.mashibing.com";
public InsertCommand(Content c) {
this.c = c;
}
@Override
public void doit() {
c.msg = c.msg + strToInsert;
}
@Override
public void undo() {
c.msg = c.msg.substring(0, c.msg.length()-strToInsert.length());
}
}
请求者角色: ContextInvoker
搭配责任链模式: 加载一系列命令,且实现实现多次undo
/**
* 请求者角色,调用命令对象执行这个请求
*/
public class ContextInvoker {
//责任链模式
private List<Command> commands = new ArrayList<>();
public ContextInvoker(List<Command> commandList) {
this.commands = commandList;
}
/**
* 执行指定命令
*/
public void execute(Command command){
command.doit();
}
/**
* 执行所有doit命令
*/
public void doitAll(){
for (Command comm : commands){
comm.doit();
}
}
/**
* 执行所有undo命令
*/
public void undoAll(){
for(int i= commands.size()-1; i>=0; i--) {
commands.get(i).undo();
}
}
}
客户角色: 测试 Main
public class Main {
public static void main(String[] args) {
//创建一个接收者对象
Content c = new Content();
System.out.println("原始文字内容: ==> "+c.msg);
//创建一系列命令,来对接收者执行操作
List<Command> commands = new ArrayList<>();
commands.add(new InsertCommand(c));
commands.add(new CopyCommand(c));
commands.add(new DeleteCommand(c));
//创建一个请求者角色
ContextInvoker invoker = new ContextInvoker(commands);
//发送命令doit ,执行一系列命令
invoker.doitAll();
System.out.println("执行插入,复制,删除后的内容: ==> "+c.msg);
//发送命令undo ,执行操作回滚
invoker.undoAll();
System.out.println("执行undo后的内容: ==> "+c.msg);
}
}
源码中命令模式体现
1. JDK源码中命令模式体现:
首先来看 JDK 中的 Runnable 接口,Runnable 相当于命令模式中的抽象命令角色。Runnable 中的 run() 方法就当于 doit() 方法。
public interface Runnable {
public abstract void run();
}
public class T implements Runnable {
@Override
public void run() {
LazySingleton lazySingleton = LazySingleton.getInstance();
System.out.println(Thread.currentThread().getName() + " : " + lazySingleton);
}
}
public class Test {
public static void main(String[] args) {
Thread t1 = new Thread(new T());
Thread t2 = new Thread(new T());
t1.start();
t2.start();
System.out.println("Program End");
}
}
- 只要是实现了 Runnable 接口的类都被认为是一个线程,相当于命令模式中的具体命令角色。
- 实际上调用线程的 start() 方法之后,就有资格去抢 CPU 资源,而不需要编写获得 CPU 资源的逻辑。而线程抢到 CPU资源后,就会执行 run() 方法中的内容,用 Runnable 接口把用户请求和 CPU 执行进行解耦。
用户请求 ==> 请求者角色
CPU ==> 执行者角色
Runnable 接口实现类 ==>具体命令角色
2. JUnit源码中命令模式体现:
使用JUnit做但单元测试的时候,只需实现Test接口,加上相应注解,就可以测试现有的类,而无须修改现有的类;
- 下面再来看大家非常熟悉的 junit.framework.Test 接口。
- Test 接口中有两个方法,第一个是 countTestCases() 方法,用来统计当前需要执行的测试用例总数。第二个是 run()方法,用来执行具体的测试逻辑,其参数 TestResult 用来返回测试结果。
实际上,平时我们在编写用例时,只需要实现 Test 接口就会被认为这是一个测试用例,执行时就会被自动识别。通常做法都是继承 TestCase 类,TestCase 类的源码如下。
- 可以看出,TestCase 类也实现了 Test 接口。我们继承 TestCase 类,相当于也实现了 Test接口,自然会被扫描成为一个测试用例。
3. Struts2中action中的调用过程中存在命令模式:
4. 数据库中的事务机制的底层实现
总结:
使用场景:
- 现实语义中存在具备“命令”的操作,如:dos命令,shell命令
- 数据库中的事务机制的底层实现
- 需要支持命令宏(即命令组合)操作。
- 命令的撤销和恢复:增加相应的撤销和恢复命令的方法(比如数据库中的事务回滚)
命令模式和其他模式搭配使用:
优点:
- 通过引入中间件(抽象接口),解耦了命令请求与实现。降低了系统耦合度
- 扩展性良好,可以很容易地增加新命令。新的命令可以很容易添加到系统中去。
- 支持组合命令,支持命令队列。
- 可以在现有命令的基础上,增加额外功能,比如日志记录,结合装饰器模式会更加灵活
缺点:
- 具体命令类可能过多。
- 命令模式的结果其实就是接收方的执行结果,但是为了以命令的形式进行架构、解耦请求与实现,引入额外类型结构(引入请求方与抽象命令接口)。
- 增加了理解上的困难。不过这也是设计模式的通病抽象必然会额外增加类的数量;代码抽离肯定比代码聚合更难理解。