早期的黑白电视机要换台那简直是很不容易,需要跑到电视机前面扳动上面那个切换频道的按钮,一顿折腾下来才能完成一次换台。现如今,我们只需要躺在沙发上按一下遥控器的按钮就可以轻松的躺在沙发上完成一次次的换台了。这里就使用了命令模式,将换台命令和换台处理进行了分离。
还有就是餐厅的点菜单,一般是后厨先把所有的原材料组合配置好了,客户需要用餐只需要点菜即可,将需求和处理进行了解耦。
命令模式(Command Pattern)是对命令的封装,每一个命令都是操作:请求一方发出请求要求执行一个操作;接收一方收到请求,并执行操作。命名模式解耦了请求方和接收方,请求方只需要请求执行命令,不用关系命令是怎样被接收,怎样被操作以及是否被执行等等。
一、命令模式的应用场景
当系统的某项操作具备命令语义时,且命令实现不稳定,那么可以通过命令模式解耦请求与实现,利用抽象命令接口使请求方代码架构稳定,封装接收方具体命令实现细节。命令模式适用于以下几个场景:
如果自己开发一个音乐播放器,它的功能有开始播放功能、暂停播放功能、停止播放功能、拖动进度条功能,自己去操作播放器的时候并不是直接调用播放器的方法,而是通过一个控制条传达指令给播放器的内核,那么具体的指令会封装成一个个按钮。那么每一个按钮就相当于是对一条命令的封装。用控制条实现了用户发送指令与播放器内核接收指令的解耦。 首先创建播放器内核Player类:
public class Player {
public void play() {
System.out.println("播放");
}
public void pause() {
System.out.println("暂停");
}
public void stop() {
System.out.println("停止");
}
public void speed() {
System.out.println("拖动进度条");
}
}
创建命令接口ICommand:
public interface ICommand {
void execute();
}
然后分别创建操作播放器可以接收的指令,播放指令PlayCommand类:
public class PlayCommand implements ICommand {
private Player player;
public PlayCommand(Player player) {
this.player = player;
}
@Override
public void execute() {
player.play();
}
}
暂停指令PauseCommand类:
public class PauseCommand implements ICommand {
private Player player;
public PauseCommand(Player player) {
this.player = player;
}
@Override
public void execute() {
player.pause();
}
}
停止指令StopCommand类:
public class PauseCommand implements ICommand {
private Player player;
public PauseCommand(Player player) {
this.player = player;
}
@Override
public void execute() {
player.pause();
}
}
拖动进度条指令SpeedCommand类:
public class SpeedCommand implements ICommand {
private Player player;
public SpeedCommand(Player player) {
this.player = player;
}
@Override
public void execute() {
player.speed();
}
}
最后,创建控制条Controller类:
public class Controller {
private List<ICommand> commands = new ArrayList<>();
public void addCommand(ICommand command) {
commands.add(command);
}
public void execute(ICommand command) {
command.execute();
}
public void executes() {
for(ICommand command : commands) {
command.execute();
}
commands.clear();
}
}
测试main方法:
public static void main(String[] args) {
Player player = new Player();
Controller controller = new Controller();
controller.addCommand(new PlayCommand(player));
controller.addCommand(new PauseCommand(player));
controller.addCommand(new StopCommand(player));
controller.addCommand(new SpeedCommand(player));
controller.executes();
}
由于控制条已经与播放器内核解耦,以后想扩展新的命令,只需要增加命令即可,无需改动控制条结构。
二、命令模式在源码中的体现
2.1 Runnable接口
实际上Runnable接口就相当于是命令的抽象,只要是实现了Runnable接口的类都被认为是一个线程。
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();
}
实际上调用的线程的start方法后,就有资格去抢CPU的资源了,而不需要我们编写获得CPU资源的逻辑。而线程抢到CPU资源后,就会去执行run()方法中的内容,用Runnable接口把用户请求和CPU执行进行了解耦。
2.2 junit.framework.Test接口
先来看接口源码:
package junit.framework;
public interface Test {
int countTestCases();
void run(TestResult var1);
}
上面Test接口中有两个方法,第一个countTestCases()方法用来统计当前需要执行的测试用例总数。第二个run()方法用来执行具体的测试逻辑,其参数TestResult用来返回测试结果的。实际上我们平时在编写测试用例的时候,只需要实现Test接口即便认为就是一个测试用例,那么在执行的时候就自动识别了。平时的通常做法就是继承TestCase类,来看下它的源码:
public TestResult run() {
TestResult result = this.createResult();
this.run(result);
return result;
}
public void run(TestResult result) {
result.run(this);
}
实际上TestCase类它也实现了Test接口。我们继承了TestCase类也相当于实现了Test接口,自然也会被扫描成一个测试用例。
三、命令模式的优缺点
优点:
- 通过引入中间件(抽象接囗),解耦了命令请求与实现;
- 扩展生良好,可以很容另地增加新命令;
- 支持组合命令,支持命令队列;
- 可以在现有命令的基础上,增加额外功能(比如日志记录等,结合装饰器模式更酸爽)。
缺点:
- 具体命令类可能过多;
- 命令模式的结果其实就是接收方的执行结果,但是为了以命令的形式进行架构,解耦请求与实现,引入了额外类型结构(引入了请求方与抽象命令接囗),增加了理解上的困难(不过这也是设计模式带来的一个通病,抽象必然会引入额外类型;抽象肯定比紧密难理解)。