设计模式——命令模式 (Command Pattern)


一、命令模式的概念

命令模式的定义:将请求封装成对象,以便使用不同的请求、队列或日志来参数化其他对象。命令模式也支持可撤销的操作。

命令模式的目的:是解除调用类与接收者类之间的耦合。

命令模式理解引导:看电视时,我们点击遥控器的换台按钮,电视机就会转换到相应的频道。在这个最典型的例子中,我们(Client)发出一条命令(Commend)给遥控器(Invoker),然后遥控器接着通知电视机(Receiver)执行该命令,这就是命令模式的表现。让电视机转换频道这个动作的调用者本该是我们,而我们则把这个调用的动作交给了遥控器来达到调用的目的,然而无论如何都是电视机自己转换了频道,我们或遥控器只是决定了电视机转换频道的时机。我们不直接调用电视机的调频功能就能调频,这样就实现了调用者与接收者的解耦。

命令模式的构成

        1Client做初始化,实例化所有将要执行的Commend对象,和提供对应功能的Receiver(即TV)对象。

        2Commend是命令接口,其子类负责提供请求的处理功能,但他不包含功能的具体实现。

        2Invoker是控制类,决定何时调用某功能。

        3ReceiverTV)是所有命令的功能代码的具体实现类。


命令模式的UML类图

二、命令模式的java实现

通过java代码实现遥控电视的命令模式:

具体的接收者,是命令的执行者。

/**
 * 命令的接收者,实现具体的命令代码
 **/
public class TV {
	/** 
	 * 具体的开机操作实现
	 * */
	public void open() {
		
		System.out.println("\n----开机操作----");
		System.out.println("检查线路");
		System.out.println("连接电源");
		System.out.println("播放");
		System.out.println("----开机完毕----\n");
		
	}

	/**
	 * 具体的关机操作实现
	 *  */
	public void close() {
		
		System.out.println("\n----关闭操作----");
		System.out.println("停止播放");
		System.out.println("切断电源");
		System.out.println("----关闭完毕----\n");
	}

	/**
	 * 具体的转换频道操作实现
	 *  */
	public void channel() {
		System.out.println("\n----切换频道操作----");
		System.out.println("正在切换频道");
		System.out.println("----切换频道完成----\n");
	}
}

命令接口,统一命令的规则。

/**
 * <span style="font-family: Arial, Helvetica, sans-serif;">命令的接口,定义命令的规则</span>
 *  */
public interface Commend {
	/** 
	 * 接口函数
	 * */
	public void exectute();
}


命令接口的具体实现类,该类持有一个具体接受者类的引用,以便调用接收者对应功能的方法。

关闭命令:

/**
 * <span style="font-family: Arial, Helvetica, sans-serif;">关闭命令类,只需知道要调用那些功能,而不需要知道这些功能的具体实现</span>
 *  */
public class CloseCommend implements Commend {
	/**
	 * 持有TV的引用
	 *  */
	private TV tv;

	public CloseCommend(TV tv) {
		this.tv = tv;
	}

	/**
	 * 调用TV中对应的操作
	 *  */
	public void exectute() {
		tv.close();
	}
}

调频命令:

/** 
 * <span style="font-family: Arial, Helvetica, sans-serif;">调频命令类,只需知道要调用那些功能,而不需要知道这些功能的具体实现</span>
 * */
public class ChannelCommend implements Commend {
	/**
	 * 持有TV的引用
	 *  */
	private TV tv;
	
	public ChannelCommend(TV tv) {
		this.tv = tv;
	}
	
	/**
	 * 调用TV中对应的操作
	 *  */
	public void exectute() {
		tv.channel();
	}
	
}

开机命令:

/**
 * <span style="font-family: Arial, Helvetica, sans-serif;">开机命令类,只需知道要调用那些功能,而不需要知道这些功能的具体实现</span>
 */
public class OpenCommend implements Commend {
	/** 
	 * 持有TV的引用
	 * */
	private TV tv;

	public OpenCommend(TV tv) {
		this.tv = tv;
	}

	/**
	 * 调用TV中对应的操作
	 *  */
	public void exectute() {
		tv.open();
	}
}


调用者,通过他来实现调用与执行分离。

/**
 * 调用者
 * */
public class Invoker {
	/**
	 * 持有命令的父接口引用
	 * */
	private Commend com;

	public void setCom(Commend com) {
		this.com = com;
	}

	/**
	 * 执行命令中的方法
	 * */
	public Invoker(Commend c) {
		this.com = c;
	}

	public void call() {
		com.exectute();
	}

}


客户类,负责创建命令,接受者,和调用者。

/** 
 * 客户类
 * */
public class Client {
	public static void main(String[] args) {
		//创建TV对象
		TV tv = new TV();
		//创建开机命令
		Commend open = new OpenCommend(tv);
		Invoker iv = new Invoker(open);
		iv.call();
		//创建调频命令
		Commend channel = new ChannelCommend(tv);
		iv.setCom(channel);
		iv.call();
		//创建关机命令
		Commend close = new CloseCommend(tv);
		iv.setCom(close);
		iv.call();
		
	}
}

执行结果:

三、命令模式的进阶

        在了解基本的命令模式的实现之后,我们再对命令模式进行一些功能性的扩充,实现一些高级的操作。比如在调用者中存储多个命令,实现命令的撤销等

3.1、存储多个命令

        你肯定会发现这样和直接调用电视机的相应功能没什么区别,对没错,这只是演示最基本的命令模式。上面的Invoker,只能通过不断的装载新的命令来调用新的功能,并不像遥控器那样可以执行不同的命令,遥控器如果是这样就没有什么意义了。下面来添加一些功能来实现一个较为真实的遥控器。

修改后的Invoker

/**
 * 调用者
 */
public class Invoker {
	/**
	 * 持有持有多个命令接口的引用
	 */
	private Commend[] coms;

	/**
	 * 初始化Invoker
	 */
	public Invoker() {
		// 默认可以存储三个命令
		this.coms = new Commend[3];
	}

	/**
	 * 将命令设置到对应的位置上
	 * 
	 * @param com
	 * @param index
	 */
	public void setCom(Commend com, int index) {
		coms[index] = com;
	}

	/**
	 * 执行对应命令中的方法
	 */
	public void call(int index) {
		coms[index].exectute();
	}
}
对应的Client:

/**
 * 客户类
 */
public class Client {
	public static void main(String[] args) {
		// 创建TV对象
		TV tv = new TV();
		// 创建一个遥控器Invoker
		Invoker iv = new Invoker();
		// 创建开机命令
		Commend open = new OpenCommend(tv);
		// 创建调频命令
		Commend channel = new ChannelCommend(tv);
		// 创建关机命令
		Commend close = new CloseCommend(tv);
		iv.setCom(open, 0);
		iv.setCom(channel, 1);
		iv.setCom(close, 2);
		Scanner sc = new Scanner(System.in);
		do {
			System.out.print("请输入对应index的命令:");
			iv.call(sc.nextInt());
		} while (true);

	}
}
执行Client得到:



怎么样?通过在Invoker中添加一个数组来存储多个命令来分别调用对应的命令,现在这个命令模式是不是有点像遥控器了。不仅如此,我们还可以动态的将对应的位置的命令 进行 替换,实现“按钮”与功能的动态绑定。只不过这个实现比较简陋但是已经足以表示其作用了。

3.2、实现撤销功能(undo)

        在很多遥控器上都能看到撤销这个功能,点击撤销就会将上次的动作还原,这个“还原”相对来说就是与上个命令执行结果相反的操作。那么怎么做才能达到这个目的尼?很显然,这个相反的操作必须由电视机(Receiver)提供,而触发触发他则是遥控器(Invoker),而我们(Client)则需要创建这样一条支持撤销的命令(Commend)给遥控器。由此看来我必须重新处理下这个命令模式的内容,如果支持撤销,那么该命令必须提供与execute()方法相反的undo()方法,无论execute()刚做过什么,undo()都会还原。so在Commend中加入undo()方法。

修改后的Commend接口:

/**
 * 命令的接口,定义命令的规则
 */
public interface Commend {
	/**
	 * 接口函数
	 */
	public void exectute();
	/**
	 * 将exectute()方法所做的事情还原
	 */
	public void undo();
}

Commend接口发生了变化,那么实现了该接口的具体命令也将实现该方法。对于OpenCommend来说,execute()方法是开电视,那么undo()方法则是关电视。以此类推,CloseCommend的undo()方法则是开电视,ChannelCommend的undo方法则是调到上一个频道,TV添加一个回调功能用于支持undo。在此仅贴出OpenCommend的具体修改,相信其他部分也能很随意的写出来。

修改后的OpenCommend命令:

/**
 * 开机命令类,只需知道要调用那些功能,而不需要知道这些功能的具体实现
 */
public class OpenCommend implements Commend {
	/**
	 * 持有TV的引用
	 */
	private TV tv;

	public OpenCommend(TV tv) {
		this.tv = tv;
	}

	/**
	 * 调用TV中对应的操作
	 */
	public void exectute() {
		tv.open();
	}

	@Override
	public void undo() {
		tv.close();
	}
}
接收者、命令都有了,那么现在就要让调用者(Invoker)支持undo这个功能了。那么问题来了要怎么实现?相比大家都已经知道了,对没错,就是在Invoker中用一个新的字段将记录上一次操作的命令是什么,然后在撤销时候直接执行该命令的undo方法就可以了,是不是很简单。

修改后的Invoker类:(这里的NothingCommend是Commend接口的空实现用来用其初始化操作,防止出现空指针异常,其他避免异常的操作亦可,推荐使用如下使用方法)

/**
 * 调用者
 */
public class Invoker {
	/**
	 * 持有持有多个命令接口的引用
	 */
	private Commend[] coms;
	private Commend preCom;

	/**
	 * 初始化Invoker
	 */
	public Invoker() {
		// 默认可以存储三个命令
		this.coms = new Commend[3];
		//创建一个空的命令进行初始化 避免空指针异常
		NothingCommend nCommend = new NothingCommend();
		for (int i = 0; i < coms.length; i++) {
			coms[i] = nCommend;
		}
		//刚开始也没有前一个命令,则也用命令初始化
		preCom = nCommend;
	}

	/**
	 * 将命令设置到对应的位置上
	 * 
	 * @param com
	 * @param index
	 */
	public void setCom(Commend com, int index) {
		coms[index] = com;
	}

	/**
	 * 执行对应命令中的方法
	 */
	public void call(int index) {
		coms[index].exectute();
		preCom = coms[index];
	}

	/**
	 * 撤销上一个命令的动作
	 */
	public void undoCall() {
		preCom.undo();
	}
}

Client做出了些简单调整如下:

/**
 * 客户类
 */
public class Client {
	public static void main(String[] args) {
		// 创建TV对象
		TV tv = new TV();
		// 创建一个遥控器Invoker
		Invoker iv = new Invoker();
		// 创建开机命令
		Commend open = new OpenCommend(tv);
		// 创建调频命令
		Commend channel = new ChannelCommend(tv);
		// 创建关机命令
		Commend close = new CloseCommend(tv);
		iv.setCom(open, 0);
		iv.setCom(channel, 1);
		iv.setCom(close, 2);
		Scanner sc = new Scanner(System.in);
		String com;
		do {
			System.out.print("请输入对应index的命令(undo为撤销操作):");
			com = sc.nextLine();
			if ("undo".equals(com)) {
				iv.undoCall();
			} else {
				iv.call(Integer.parseInt(com));
			}
		} while (true);

	}
}

执行Client后的结果如下:


是不是很简单就实现撤销操作?该例子的撤销功能还是比较简单,还以通过一个数组、队列或栈来将操作过的命令进行记录来实现多次撤销,这个实现起来也是很简单的,在此只做抛砖引玉大家可以自行实现。

3.3组合命令

        现在,我们想实现打开电视后立即跳转到指定频道这个功能,用上面的命令模式实现还需要两个命令OpenCommend和ChannelCommend,然而我很懒只想动一下,那么该怎么办尼?此时组合命令就派上用场了,顾名思义,就是将多个命令组合起来使用,组合后的命令依然属于命令,所以仍然可以像其他命令一样使用。还拿上面的例子来说(呵呵,例子太简单了),我们要将OpenCommend和ChannelCommend这两个命令包装成一个命令即可。有了这个想法那么实现起来也是很简单的,我们只需要重新实现也Commend接口,该接口需要用其他一组命令来初始化,然后在execute()中顺序的执行这组命令的execute()方法,同理undo方法也是(不过undo要反过来执行,为什么?),好了是时候实现该命令了(果然懒惰是人类进步的阶梯,哈哈......)。

组合命令的实现如下:

/**
 * 组合命令
 * 
 * @author Administrator
 *
 */
public class CombinatonCommend implements Commend {
	/**
	 * 用来记录要组合执行的所以命令
	 */
	private Commend[] coms;

	/**
	 * 通过构造函数对coms初始化
	 * 
	 * @param coms
	 */
	public CombinatonCommend(Commend[] coms) {
		super();
		this.coms = coms;
	}

	/**
	 * 执行coms中所有命令的exectute方法
	 */
	@Override
	public void exectute() {
		for (int i = 0; i < coms.length; i++) {
			coms[i].exectute();
		}
	}

	/**
	 * 执行coms中所有命令的undo方法
	 */
	@Override
	public void undo() {
		for (int i = coms.length - 1; i >= 0; i--) {
			coms[i].undo();
		}
	}
}
只需对Client进行简单的修改即可测试组合命令,此处只是将index=0处的命令改成了包含了开机与调频的组合命令,只贴出运行结果:



3.4命令模式的其他用途

        命令请求队列:创建出来的命令可以立即执行,也可以延后执行,也可以不执行,更可以在其他线程中执行(需考虑线程安全的问题),这样我们可以将需要执行的命令放到一个队列当中,前面对命令进行处理,后面还可以继续添加命令。还有日志请求等。

四、命令模式的总结

命令模式的优点:

1、命令模式可以实现调用者和接受者之间的解耦。

2、命令模式很容易扩充,新增Command,并且无需改变现有的类。

3、可以组合命令

4、支持命令的撤销与重做等

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值