命令模式是一种原理非常简单,但是使用起来非常优雅、方便的并且个人觉得很有艺术感的设计模式。
一、介绍
还是先来看一下《研磨设计模式》的定义——将一个请求封装为一个对象,从而使你可用不同的请求对客户进行参数化,对请求排队或记录请求日志,以及支持可撤销的操作。
命令模式的本质——封装请求。
封装请求是什么意思呢?其实在Java中随处可见。最简单的就像在Swing中,每次触发一个事件,都会产生一个相应的事件对象,这个事件对象封装了这个事件所有的参数,我们通过这个事件对象来对这个事件进行处理。同样的,我们每次触发了一个异常,这个异常的所有参数、包括堆栈信息都会被封装在一个异常对象中,它代表这个异常事件本身,然后由我们来处理或抛出。
这样讲来,命令模式不过是将一个请求封装起来罢了。然而,这里面却大有文章可作。
二、我的实现
我们在写文档的时候,作为命令对象接受者的文本文档,它可能接收的是我们在键盘的一个简单的字符输出的命令,有可能接收的是改变光标位置的命令,也可能接收的是我们的删除命令;在进行这些操作的时候,也有可能接收的是我们的撤销操作命令。假设我们的文本文档只能接收这些命令。
现在我们用命令模式封装这些命令模拟一下在文档上进行文本操作的过程。1、首先我们有一个命令接口,如下:
1 public interface Command { 2 3 //处理 4 public void execute(); 5 //恢复 6 public void recover(); 7 }
2、按照计算机的原理,我们对文本操作的命令应该是键盘或鼠标将命令发送给外设驱动,外设驱动发送给CPU,然后CPU发送给显卡,显卡发送给屏幕,进行显示。这里我们省略繁琐的命令传递,将命令直接交给屏幕。由于对屏幕的操作都是属于屏幕本身的操作,这里不用接口了,构建屏幕类如下:
1 public class Screen { 2 3 //设置为单例 4 private static Screen screen = new Screen(); 5 //当前屏幕内容 6 private StringBuilder screenContent = new StringBuilder(); 7 //此次刷新前屏幕内容 8 private StringBuilder oldContent = screenContent; 9 //屏幕光标位置,默认为屏幕内容最尾 10 private int pos = screenContent.length(); 11 12 private Screen(){ 13 } 14 15 public static Screen getInstance(){ 16 return screen; 17 } 18 //指定位置插入字符 19 public void insert(int pos, String content){ 20 oldContent = screenContent; 21 screenContent.insert(pos, content); 22 this.pos = pos + content.length(); 23 } 24 //插入字符 25 public void insert(String content){ 26 this.insert(pos,content); 27 } 28 29 //返回当前光标位置 30 public int getPos() 31 { 32 return pos; 33 } 34 35 //设置当前光标位置 36 public void setPos(int pos) 37 { 38 this.pos = pos; 39 } 40 41 //返回屏幕内容 42 public String getScreenContent() 43 { 44 return screenContent.toString(); 45 } 46 //可用来覆盖屏幕内容 47 public void setScreenContent(String src) 48 { 49 oldContent = screenContent; 50 this.screenContent = new StringBuilder(src); 51 pos = src.length(); 52 } 53 54 //返回此次刷新前屏幕内容 55 public String getOldContent() 56 { 57 return oldContent.toString(); 58 } 59 60 //清空屏幕 61 public void clear(){ 62 oldContent = screenContent; 63 screenContent = new StringBuilder(); 64 pos = 0; 65 } 66 67 //输出 68 public void outPut(){ 69 System.out.println(new SimpleDateFormat("hh:mm:ss").format(new Date())+",当前光标位置:"+pos+",屏幕刷新输出:"+screenContent); 70 } 71 72 73 }
3、好了,我们来构建第一个书写命令,如下:
1 public class WriteCommand implements Command { 2 // 写之前屏幕的内容 3 private String oldScreenContent; 4 //移动光标前光标位置 5 private int oldPos; 6 //要写的 7 private String content; 8 private Screen screen = Screen.getInstance(); 9 10 public void setScreen(Screen screen) 11 { 12 this.screen = screen; 13 } 14 15 //构造方法传入要写的内容 16 public WriteCommand(String str) 17 { 18 this.content = str; 19 } 20 21 // 执行 22 @Override 23 public void execute() 24 { 25 System.out.println("-----------------------写命令!内容为:"+content); 26 oldPos = screen.getPos(); 27 oldScreenContent = screen.getScreenContent(); 28 // 屏幕追加新内容 29 screen.insert(content); 30 // 屏幕刷新输出 31 screen.outPut(); 32 } 33 34 // 恢复 35 @Override 36 public void recover() 37 { 38 System.out.println("-----------------------写命令!撤销内容为:"+content); 39 screen.setScreenContent(oldScreenContent); 40 screen.setPos(oldPos); 41 // 屏幕刷新输出 42 screen.outPut(); 43 } 44 45 }
可以看到,这个命令持有的属性包括输入的内容、输入前光标位置、输入前屏幕的内容和输入的屏幕。
4、同样的,我们可以构建一个移动光标的命令,如下:
1 //移动光标命令 2 public class MoveCursorCommand implements Command { 3 4 //移动光标前光标位置 5 private int oldPos; 6 //移动光标后光标位置 7 private int currentPos; 8 9 private Screen screen = Screen.getInstance(); 10 11 public void setScreen(Screen screen) 12 { 13 this.screen = screen; 14 } 15 16 public MoveCursorCommand(int currentPos) 17 { 18 this.currentPos = currentPos; 19 } 20 21 @Override 22 public void execute() 23 { 24 System.out.println("-----------------------移动光标命令,光标移动至" + currentPos); 25 oldPos = screen.getPos(); 26 screen.setPos(currentPos); 27 screen.outPut(); 28 } 29 30 @Override 31 public void recover() 32 { 33 System.out.println("-----------------------移动光标命令,恢复光标至" + oldPos); 34 screen.setPos(oldPos); 35 screen.outPut(); 36 } 37 38 }
可以看到,这个命令持有的属性包括移动前光标的位置、移动后光标的位置,还有输出的屏幕。
5、接下来构建一个删除的命令,如下:
1 public class DeleteCommand implements Command { 2 3 //要删除的长度 4 private int deleteLength; 5 //删除前屏幕内容 6 private String oldScreenContent; 7 //删除前光标的位置 8 private int oldPos; 9 private Screen screen = Screen.getInstance(); 10 11 public void setScreen(Screen screen) 12 { 13 this.screen = screen; 14 } 15 16 //构造方法,传入要删除的长度 17 public DeleteCommand(int deleteLength) 18 { 19 this.deleteLength = deleteLength; 20 } 21 22 // 默认删除一格 23 public DeleteCommand() 24 { 25 this(1); 26 } 27 28 @Override 29 public void execute() 30 { 31 System.out.println("-----------------------删除命令!删除"+deleteLength+"个字符!"); 32 oldPos = screen.getPos(); 33 oldScreenContent = screen.getScreenContent(); 34 // 如果删除长度大于当前光标位置,删除光标前所有内容 35 if (deleteLength >= screen.getPos()) 36 { 37 screen.setScreenContent(screen.getScreenContent().substring(screen.getPos())); 38 // 否则 39 } 40 else 41 { 42 String newContent = screen.getScreenContent().substring(screen.getPos()) + screen.getScreenContent().substring(0, screen.getPos() - deleteLength); 43 screen.setScreenContent(newContent); 44 } 45 screen.outPut(); 46 } 47 48 @Override 49 public void recover() 50 { 51 System.out.println("-----------------------删除命令!恢复所删除的"+deleteLength+"个字符!"); 52 screen.setScreenContent(oldScreenContent); 53 screen.setPos(oldPos); 54 screen.outPut(); 55 } 56 57 }
可以看到,这个命令对象持有属性包括要删除的长度、删除前屏幕的内容、删除前光标的位置和输出的屏幕。
我们可以看到,每个命令的属性都不尽相同,都包含了其主要的特征。而真正操作这些命令的,是屏幕类。6、下面进行简单的测试:
1 public class Test { 2 3 public static void main(String[] args) 4 { 5 //写命令 6 Command write = new WriteCommand("abcd"); 7 //移动光标命令 8 Command moveCursor = new MoveCursorCommand(3); 9 //写命令 10 Command write2 = new WriteCommand("123456789"); 11 //删除命令 12 Command delete = new DeleteCommand(5); 13 14 //执行这些命令 15 write.execute(); 16 moveCursor.execute(); 17 write2.execute(); 18 delete.execute(); 19 //恢复一下 20 write2.recover(); 21 } 22 }
6、结果如下:
-----------------------写命令!内容为:abcd 05:11:06,当前光标位置:4,屏幕刷新输出:abcd -----------------------移动光标命令,光标移动至3 05:11:06,当前光标位置:3,屏幕刷新输出:abcd -----------------------写命令!内容为:123456789 05:11:06,当前光标位置:12,屏幕刷新输出:abc123456789d -----------------------删除命令!删除5个字符! 05:11:06,当前光标位置:8,屏幕刷新输出:dabc1234 -----------------------写命令!撤销内容为:123456789 05:11:06,当前光标位置:3,屏幕刷新输出:abcd
可见,每一步命令操作都保存了操作前的屏幕状态,可恢复到任意操作处去。
这种方式有一个缺点,那就是每次操作前都保存了屏幕的所有状态,会消耗大量内存,所以,我们可以用一个队列列表来放这些命令,列表容量是可设置的。超过容量的操作不再保存,即不可恢复。同时这也有一个优点,那就是可以很方便的一键撤销上一步操作。而这个就是命令模式的队列请求。下面我们来试一下。
三、队列请求
1、命令队列如下:
1 public class CommandQueue { 2 3 private static LinkedList<Command> commands = new LinkedList<Command>(); 4 5 public synchronized static void addAndExecute(Command cmd){ 6 //大于10,就移除第一个 7 if(commands.size() > 10) { 8 commands.remove(); 9 } 10 commands.add(cmd); 11 cmd.execute(); 12 } 13 14 public synchronized static void recover(Command cmd){ 15 if(cmd == null){ 16 System.out.println("命令不存在!"); 17 }else { 18 cmd.recover(); 19 } 20 } 21 }
2、测试代码也要改一下:
1 public class Test { 2 3 public static void main(String[] args) 4 { 5 //写命令 6 Command write = new WriteCommand("abcd"); 7 //移动光标命令 8 Command moveCursor = new MoveCursorCommand(3); 9 //写命令 10 Command write2 = new WriteCommand("123456789"); 11 //删除命令 12 Command delete = new DeleteCommand(5); 13 14 //将这些命令依次放入命令列表并执行 15 CommandQueue.addAndExecute(write); 16 CommandQueue.addAndExecute(moveCursor); 17 CommandQueue.addAndExecute(write2); 18 CommandQueue.addAndExecute(delete); 19 20 //恢复一下 21 CommandQueue.recover(write2); 22 } 23 }
结果与之前的一样。
可以看到,将请求封装之后,我们能够做的事情就太多了。我们还可以将请求打包,批量执行。还可以将请求对象持久化以便随时恢复。这里就不必演示了。