本文通过解决老王经常搞错借书人的问题,来引出行为型模式中的命令模式。为了在案例之上理解的更加透彻,我们需要了解命令模式在源码中的应用。最后指出命令模式的应用场景和优缺点。
读者可以拉取完整代码到本地进行学习,实现代码均测试通过后上传到码云。
一、引出问题
老王的书房藏书越来越多,每天来借书的人络绎不绝。每天有人借书、还书、老王将A借的书算到B头上的乌龙事件频出。老王和小王就商量着手解决这个问题。
小王提议,在老王和借书者之间再增加一个“记录员”角色,记录员只管报名字就行了,具体是借什么书由借书者自己决定就好了。
老王说:这能解决部分问题。但在真实的场景下,不可能来一个借书者“记录员”就跑一趟。而且借书者有时候会借一半临时有事就不借了。这些问题你也要考虑进去。
老王接着说:你应该,在“记录员”角色中,增加一个队列,将所有借书者都放到一个队列中,既有往队列中放命令的方法,也有从命令中移除的方法,方便“记录员”请求排队和“撤销”。
二、命令模式的概念和应用
老王提出来的正是命令模式的“白话文解释”。我们来看命令模式的官方概念:将一个请求封装为一个对象,使发出请求的责任和执行请求的责任分割开,解耦合。这样两者之间通过命令对象进行沟通,这样方便将命令对象进行存储、传递、调用、增加与管理。
在命令模式中有三个角色:
抽象命令类(Command)角色: 定义命令的接口,声明执行的方法。
实现者/接收者(Receiver)(老王)角色: 接收者,真正执行命令的对象。任何类都可能成为一个接收者,只要它能够实现命令要求实现的相应功能。
具体命令(Concrete Command)(记录员)角色:具体的命令,实现命令接口;通常会持有接收者,并调用接收者的功能来完成命令要执行的操作。
我们基于概念和角色划分,实现代码:
抽象命令类:
/** * 抽象命令类 * @author tcy * @Date 25-08-2022 */public interface AbstractCommand { //只需要定义一个统一的执行方法 void execute();}
具体命令角色(老王):
/** * 具体命令 * @author tcy * @Date 25-08-2022 */public class ConcreteCommand implements AbstractCommand { //持有接受者对象 private String clent; public ConcreteCommand(String clent){ this.clent = clent; } @Override public void execute() { System.out.println("具体执行者角色(老王):"+clent+"借书..."); }}
接收者(记录员):
/** * 接收者 * @author tcy * @Date 25-08-2022 */public class ReceiverCommand { //可以持有很多的命令对象 private ArrayList commands; public ReceiverCommand() { commands = new ArrayList(); } public void setCommand(AbstractCommand cmd){ commands.add(cmd); } public void removeCommand(AbstractCommand cmd){ commands.remove(cmd); } // 发出命令 public void borrowBookMeaaage() { System.out.println("接受者角色(记录员):有人来借书啦..."); //通知全部命令 for (int i = 0; i < commands.size(); i++) { AbstractCommand cmd = commands.get(i); if (cmd != null) { cmd.execute(); } } }}
客户端(借书者):
/** * @author tcy * @Date 25-08-2022 */public class Client { public static void main(String[] args) { //创建接收者 //将订单和接收者封装成命令对象 ConcreteCommand cmd1 = new ConcreteCommand( "A"); ConcreteCommand cmd2 = new ConcreteCommand( "B"); //创建具体命令者 ReceiverCommand invoker = new ReceiverCommand(); invoker.setCommand(cmd1); invoker.setCommand(cmd2); //喊一声有人要借书 invoker.borrowBookMeaaage(); }}
基于命令模式实现的代码就实现了,但是看懂代码是一回事,自己能写出来就是另外一回事了。读者最好根据案例重新仿写一遍。
三、源码中的应用
在源码中使用命令模式的典型案例就是Jdk多线程章节中的Runnable ,Runnable 相当于命令模式中的抽象命令角色。Runnable 中的 run() 方法就当于 execute() 方法。
我们知道,Java中一个类实现Runnable 接口,那么该类就认为是一个线程,就相当于命令模式中的具体命令角色。
当我们调用start()方法后,就可以与别的线程强占CPU的资源,在占用CPU的线程中就会执行run()方法。CPU的调度者就相当于具体命令角色也即记录员。Runnable 就完美的实现了用户自定义线程和CPU的解耦合。
命令模式在Runnable 中的应用应该很好理解。
四、总结
优点很明显,解耦了命令请求与实现,很容易的可以增加新命令,支持命令队列。
但是,这样会不可避免的使具体命令类过多,增加了理解上的困难。
设计模式学到这种程度,我们就会发现设计模式不是一种单一的技术,而是各种技术的综合体。
我们在学习设计模式的时候一定不要仅局限于一种模式,而是站在一定的高度去整体衡量哪种设计模式才是最优的。
有时候我们会发现,使用设计模式会让我们的代码变得更加的复杂,但以自己目前的开发经验又不能确定是否采用设计模式是一个好的选择。
归根结低,还是我们对设计模式掌握的不够熟练,这就需要我们继续深入学习设计模式,当我的学完再回头看这些问题,就很自然的迎刃而解了。
已经连续更新了数十篇设计模式博客,推荐你结合学习。
一、设计模式概述
二、设计模式之工厂方法和抽象工厂
三、设计模式之单例和原型
四、设计模式之建造者模式
五、设计模式之代理模式
六、设计模式之适配器模式
七、设计模式之桥接模式
八、设计模式之组合模式
九、设计模式之装饰器模式
十、设计模式之外观模式
十一、外观模式之享元模式
十二、设计模式之责任链模式