最后更新:2019.11
面向对象中的使用关系,通常被称为Client/IServer(C/S) 结构。Client类体中通常会见到这样的代码:
IServer s = (IServer) God.create("server");
s.foo();
然而,框架的设计者有时候发现,Client不知道或不在乎应该依赖谁!Client不知道要依赖的类IServer,更不知道可以调用的方法foo()。例如某将军要求打下一个山头,他可不在乎谁去打;再例如将要设计按钮/MyButton类,点击按钮后,用户可能想打开对话框、保存文件或关闭程序,此时程序员不可能让MyButton依赖Java的终极类Object,那么MyButton依赖谁呢?
1.傻乎乎的幸福
傻人傻福。既然MyButton(命令发出者,调用者/Invoker)傻乎乎地不知道命令接收者/Receiver是谁,更不知道Receiver有什么方法可以调用,于是,程序员会设计一个接口如ICommand,并封装一个一般性的方法doSth()或exe(),而让MyButton不管不顾地依赖ICommand,通常,如例程4-1所示,MyButton和ICommand属于框架/底层。
package chap4.commandP.framework;
/**
* 框架中的类,设计者显然不知道点击按钮后
*
* @author yqj2065
* @version (a version number or a date)
*/
public abstract class MyButton{
public void click(Command c){
c. exe();
}
}
package chap4.commandP.framework;
public interface Command{
public void exe();
}
由应用程序员实现的ICommand的子类型被称为子命令,指定MyButton需要依赖的某个服务类并调用服务类的某个方法。例如,服务类可能是一个对话框MyDialog,或者Java的System类(将关闭程序)等等;而子命令如SaveCommand,将指定MyDialog完成打开对话框并保存文件的任务。命令模式中的服务类在GoF中被称为接收者/Receiver。接收者可以是任意的类型。
package chap4.commandP;
public class MyDialog{
public void foo(){//可以是任意的函数名
System.out.println("XDialog:文件另存为...");
}
}
package chap4.commandP;
public class SaveCommand implements chap4.commandP.framework.Command{
public void exe(){
new MyDialog().foo();
}
}
现在考虑应用程序Client如何工作。Client首先创建一个MyButton对象,它可能被贴上OK、退出等标签;再创建一个子命令SaveCommand对象,然后将两个对象关联起来。子命令指定命令接收者为MyDialog并调用它的foo()方法。另外一个子命令是退出应用程序的命令exit对象,为lambda表达式,指定命令接收者为System并调用它的exit ()方法。
在Client中通常不需要了解接收者。
package chap4.commandP;
import static yqj2065.util.Print.*;
import chap4.commandP.framework.*;
public class Client{
public static void main(String[] args) {
pln();
Command save = new SaveCommand();
MyButton btn = new MyButton(){};
pln(save.getClass().getSimpleName()+" click");
btn.click(save);
Command exit = () -> {
System.exit(0);
};
pln(exit.getClass().getSimpleName()+" click");
btn.click(exit);
}
public static void test(){
MyButton btn = new MyButton(){};
Command eat = new Command(){ //EatCommand
@Override public void exe() {
new Chowhound().eat();
}
class Chowhound{//吃货
public void eat(){pln("好吃佬吃东西");}
}
};
btn.click(eat);
btn.click(()->{pln("吃自己");}); //没有接受者无所谓
}
}
命令模式的角色:
- Command:例子中为ICommand,定义一个通用的接口。
- Invoker:例子中为MyButton,它只会调用Command的接口。
- ConcreteCommand:例子中为X1/SaveCommand、XSystem/匿名类,改写Command接口时,调用某个消息接收者的某个方法。
- Receiver:任何类都可能成为一个接收者。
- Client:创建各种具体的命令对象,还可能使用Receiver或Invoker。
很多人认为命令模式的优点,是完成消息发出者与执行者(Invoker/ Receiver)之间的解耦,并认为采用命令模式使得软件有更松散的耦合。从结果上看有一点道理,但事实上,使用命令模式不是希望Invoker和Receiver离婚,Invoker只有10岁,他完全就不知道它老婆将会是谁。耦都没有,咋解。
命令模式的意图,形象地说,是单身狗的幸福。通常而言,阅读应用了命令模式的源代码时,阅读者不会将Invoker与某个被隔离的Receiver联系起来。
Command模式 = 单身狗的幸福。
-------------------
3.万能适配目标?通用策略?
站在X的角度,它有方法doSth(),而消息接收者千奇百怪,有System、有客户自定义的XDialog,我们是否可以将X1视为XDialog的某个(被调用)方法适配器,XSystem视为System.exit(0的适配器呢?
不管消息接收者有什么方法,通通适配成doSth()。
单纯从类图/结构上看(不管意图),MyButton-X-X1-XDialog 与对象适配器的类图完全一样。
《设计模式》中,有一句话:“命令模式正是回调机制的一个面向对象的替代品”。这句话很不合适。什么是回调机制(Call back)中说明,更一般地,回调机制可以理解为:在设计框架时使用高阶函数。在面向对象中,回调机制 = 框架中使用多态,也可以说使用策略模式。所以,我们说“策略模式是C语言回调机制的一个面向对象的替代品”也好过《设计模式》的说法。换言之,单纯看X与其子类型的关系,我们可以说这里使用了策略模式。
4.Invoker与Client角色
在简单的介绍Command模式的程序中,可能出现Client使用Invoker(如上面例子中的App使用了MyButton),甚至可能直接将调用者与客户类合二为一。
如果底层框架能够调用btn.click(x)从而底层框架能够调用x.doSth(),程序员编写的程序通常就是客户类Client,而Client不依赖Invoker。
为了演示Client不依赖Invoker,我们借助GUI框架,其中java.awt.event.ActionListener就是抽象命令角色。
package java.awt.event;
import java.util.EventListener;
public interface ActionListener extends EventListener {//EventListener 是一个标记接口
public void actionPerformed(ActionEvent e);
}
EventListener 是一个标记接口
public void actionPerformed(ActionEvent e);
}
在GUI中有1个Button,2个MenuItem——"Open..."和"Exit",先设计一个ActionListener的独立子类型
package cmd;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
public class Exit implements ActionListener {
@Override public void actionPerformed(ActionEvent e) {
System.exit(0);
}
}
而在图形界面中,定义了ActionListener的匿名类和lamdba表达式。
package cmd;
import java.awt.Button;
import java.awt.Color;
import java.awt.FileDialog;
import java.awt.Frame;
import java.awt.Menu;
import java.awt.MenuBar;
import java.awt.MenuItem;
import java.awt.Panel;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
public class ActionCommandDemo extends Frame {
public ActionCommandDemo() {
super("Command");
MenuBar mbar = new MenuBar();
setMenuBar(mbar);
Menu mnuFile = new Menu("File", true);
mbar.add(mnuFile);
MenuItem mnuOpen = new MenuItem("Open...");
mnuFile.add(mnuOpen);
MenuItem mnuExit = new MenuItem("Exit");
mnuFile.add(mnuExit);
mnuOpen.addActionListener(new ActionListener() {
@Override public void actionPerformed(ActionEvent e) {
FileDialog fDlg = new FileDialog(ActionCommandDemo.this, "Open a file", FileDialog.LOAD);
fDlg.setVisible(true);
}
});
mnuExit.addActionListener(new Exit());
Button btnRed = new Button("Red");
Panel p = new Panel();
add(p);
p.add(btnRed);
btnRed.addActionListener( e -> p.setBackground(Color.red));
setBounds(100, 100, 200, 100);
setVisible(true);
}
static public void main(String argv[]) {
new ActionCommandDemo();
}
}
ActionCommandDemo是Client角色,Exit、匿名类和lamdba表达式是ConcreteCommand,Receiver则是System、FileDialog和Panel。Invoker是谁?先不管它。
2. 命令与执行
先直接给出命令模式的例子吧。
既然有了Command,按照多态也好,难度系数为0的策略模式也罢,tv的open()演变成Command的子类OpenCommand。
OpenCommand有私有成员TV tv,而OpenCommand的exe()干什么?显然只需要一条语句tv.open()。代码自己随手写吧。
因为我们拥有依赖注入工具tool.God,(注意:在我的博客的很多的文章中,都使用了该工具,但是类名用过FromPropertyFile、IoC、God,所在包也有所变化,懒得逐一修改相关博文了。代码的意思很清楚,读者自己对应修改一下),因而代码
package method.command;
import tool.God;
public class Controller{
public static void test() {
Command c1 = (Command)God.create("open");
c1.exe();
}
}
Controller仅仅知道Command对象,Controller下的命令为字符串"open",God根据字符串"open"创建method.command.OpenCommand对象。
忽略一切细节,Controller仅依赖Command,对照的,Controller1依赖TV,和TV的现有操作/方法名。
①命令模式的核心,是封装普适方法exe ()的Command。通过它及其子类,将如图3-3所示的通常的服务请求中的请求发送者和接收者完全解耦,或者说将通常的C/S结构的C与S解耦。
C仅仅依赖于Command。而OpenCommand依赖于Command和S。
所以,我们常常说Command采用了命令模式。或许应该说 以Command同志为核心的命令模式?
②依赖于Command的各种类(不包括其子类),在《设计模式》中称为调用者(Invoker),它们是命令的发出者。借助反射机制或依赖注入模式或依赖注入工具类tool.God,调用者可以发出Command的各种子类封装的命令,而且不需要知道最终调用的是什么方法名、不需要知道最终谁执行。
如果调用者突发奇想地发出(需要)新的命令,可以编写Command的新子类以及执行者。
package method.command;
public class EatCommand implements Command{
@Override public void exe() {
new Chowhound().eat();
}
private class Chowhound{//吃货
public void eat(){System.out.println("好吃");}
}
}
在配置文件中添加eat =method.command.EatCommand
则修改Controller的"open",即c1 =(Command)God.create("eat");就ok。
③具体命令类是封装的命令的Command的各种子类,如OpenCommand。在override/改写exe ()时,将命令的执行者与某一操作绑定如tv.open()。虽然简单起见,OpenCommand中通过成员变量如电视/TV设定了执行者,事实上,可以通过依赖注入模式,按照配置文件方便地指定消息接收者的类型例如OpenHandler。
3.吐槽 《设计模式·5.2》
《设计模式》中,给命令模式(Command Pattern)的定义/意图比较繁琐。正如刀可以砍人,你把刀玩出花样来——来个回马刀都可以,刀的基本作用还是砍人。
命令模式(Command Pattern):将一个请求封装为一个对象,从而让我们可用不同的请求对客户进行参数化;对请求排队或者记录请求日志,以及支持可撤销的操作。命令模式是一种对象行为型模式,其别名为动作(Action)模式或事务(Transaction)模式。
既然命令模式使得C仅仅依赖于Command,它不知道S为何物,也不知道S的接口,所以,
C下达的一系列命令,你可以组合成一个队列、可以组合成一个批命令;也可以反之,将C下达的一个命令分解成若干具体的命令;
对于命令执行前后的变化加以监控,你可以实现undo或redo;如果命令只是改变一个页面的颜色,你很容易undo/取消操作;如果命令导致手榴弹炸了一个房屋,omg,你undo就很麻烦。
你可以玩出其他花样。比如C下达的一个命令open,对于接收者为TV,就打开电视;如果配置的接收者为一个连长,他就打开/攻占一座城门。
你可以玩出更多的花样……
续