接口模式中有三个参与者(三个对象):客户-client,调用者-invoking 和接收者-receiving。客户对象就是页面上的控件(能绑定鼠标键盘等事件的dom元素),接收用户的操作,接收者对象提供实现具体的功能的方法,命令对象与客户对象绑定,用来实现客户对象与接收者之间的低耦合,也就是弱化按钮之类的用户界面元素与其功能之间的耦合。客户对象与命令对象直接联系,它们之间的绑定必须通过接口来规范。
首先是命令模式中必须用到的接口类检查工具
/*接口定义类,命令模式必须依赖接口*//*第一个参数是接口名称,第二个参数是接口中预定义的方法*/var Interface = function(name, methods){ if(arguments.length != 2){ throw new Error('Interface constructor called with "'+ arguments.length +'" arguments, but expected exactly 2.'); } this.name = name; this.methods = []; for(var i=0,len=methods.length; i
第一个例子:
我们制作一个具有桌面应用程序风格的菜单栏,菜单栏中有多个菜单项,并通过使用命令对象,让这些菜单项执行各种操作。
/*接收者类,实现具体的操作*/var FileActions = { open: function(){alert('FileActions.open')}, close: function(){alert('FileActions.close')}, save: function(){alert('FileActions.save')}, saveAs: function(){alert('FileActions.saveAs')}};var EditActions = { cut: function(){alert('EditActions.cut')}, copy: function(){alert('EditActions.copy')}, paste: function(){alert('EditActions.paste')}, del: function(){alert('EditActions.del')}};var InsertActions = { textBlock: function(){alert('InsertActions.textBlock')}, image: function(){alert('InsertActions.image')}};var HelpActions = { showHelp: function(){alert('HelpActions.showHelp')}};/*接口定义*/var Command = new Interface('Command', ['execute']);var Composite = new Interface('Composite', ['add', 'remove', 'getChild', 'getElement']);var MenuObject = new Interface('MenuObject', ['show']);/*菜单组合对象*/var MenuBar = function(){ this.menus = {}; this.element = document.createElement('ul'); this.element.style.display = 'none';};MenuBar.prototype = {/*接口方法的实现*/ add: function(menuObject){ Interface.ensureImplements(menuObject, Composite, MenuObject); this.menus[menuObject.name] = menuObject; this.element.appendChild(menuObject.getElement()); }, remove: function(name){ delete this.menus[name]; }, getChild: function(name){ return this.menus[name]; }, getElement: function(){ return this.element; }, show: function(){ this.element.style.display = 'block'; for(var name in this.menus){ this.menus[name].show(); } }};var Menu = function(name){ this.name = name; this.items = {}; this.element = document.createElement('li'); this.element.innerHTML = this.name; this.element.style.display = 'none'; this.container = document.createElement('ul'); this.element.appendChild(this.container);/*在添加到页面之前,添加的子节点是动态改变的*/};Menu.prototype = {/*接口方法的实现*/ add: function(itemObject){ Interface.ensureImplements(itemObject, Composite, MenuObject); this.items[itemObject.name] = itemObject; //this.element.querySelector('ul').appendChild(itemObject.getElement()); this.container.appendChild(itemObject.getElement()); }, remove: function(name){ delete this.items[name]; }, getChild: function(name){ return this.items[name]; }, getElement: function(){ return this.element; }, show: function(){ this.element.style.display = 'block'; for(var name in this.items){ this.items[name].show(); } }};/*作为调用者类 *命令模式的作用在此开始显现出来。你可以创建一个包含有许多菜单的非常复杂的菜单栏,而每个菜单栏都包含着一些菜单项(MenuItem),每个MenuItem都与一个命令对象绑定在一起。这些菜单项对如何执行自己所绑定的操作一无所知,它们也不需要知道那些细节,它们唯一需要知道的就是命令对象都有一个execute方法。 */ /*第一个参数为菜单名称,第二个参数为要绑定的命令对象*/var MenuItem = function(name, command){ Interface.ensureImplements(command, Command); this.name = name; this.element = document.createElement('li'); this.element.style.display = 'none'; this.anchor = document.createElement('a'); this.anchor.innerHTML = this.name; this.anchor.href = '#'; this.element.appendChild(this.anchor); /*在事件回调中调把命令对象与调用者绑定*/ this.anchor.addEventListener('click', function(e){ e.preventDefault(); command.execute();/*就这么简单的一句*/ });};MenuItem.prototype = { /*这些方法因为在接口中有定义,必须列出来,但在这个应用中没用到,所以方法里没具体代码*/ add: function(){}, remove: function(){}, getChild: function(){}, /*实现的方法*/ getElement: function(){ return this.element; }, show: function(){ this.element.style.display = 'block'; }};/*命令类*这里的命令类非常简单。其构造函数的参数就是将被作为操作而调用的方法*/var MenuCommand = function(action){ this.action = action;};MenuCommand.prototype.execute = function(){ this.action();}/*汇总*/var fileMenu = new Menu('File');var openCommand = new MenuCommand(FileActions.open);var closeCommand = new MenuCommand(FileActions.close);var saveCommand = new MenuCommand(FileActions.save);var saveAsCommand = new MenuCommand(FileActions.saveAs);fileMenu.add(new MenuItem('open', openCommand));fileMenu.add(new MenuItem('close', closeCommand));fileMenu.add(new MenuItem('save', saveCommand));fileMenu.add(new MenuItem('saveas', saveAsCommand ));var editMenu = new Menu('Edit');var cutCommand = new MenuCommand(EditActions.cut);var copyCommand = new MenuCommand(EditActions.copy);var pasteCommand = new MenuCommand(EditActions.paste);var deleteCommand = new MenuCommand(EditActions.del);editMenu.add(new MenuItem('cut', cutCommand));editMenu.add(new MenuItem('copy', copyCommand));editMenu.add(new MenuItem('paste', pasteCommand));editMenu.add(new MenuItem('delete', deleteCommand));var insertMenu = new Menu('Insert');var textBlockCommand = new MenuCommand(InsertActions.textBlock);var imgCommand = new MenuCommand(InsertActions.image);insertMenu.add(new MenuItem('Text Block', textBlockCommand));insertMenu.add(new MenuItem('Image', imgCommand));var helpMenu = new Menu('Help');var showHelpCommand = new MenuCommand(HelpActions.showHelp);helpMenu.add(new MenuItem('Show Help', showHelpCommand));var appMenuBar = new MenuBar();appMenuBar.add(fileMenu);appMenuBar.add(editMenu);appMenuBar.add(insertMenu);document.getElementsByTagName('body')[0].appendChild(appMenuBar.getElement());appMenuBar.show();
第二个例子:
这是一个简单的html5游戏,有四个方向控制按钮和一个撤销按钮。每次点击方向按钮,会从当前位置向指定方向画出一条固定长度的线段。点击撤销按钮则可以撤销上一步操作。
撤销操作中,由于在canvas上画线的操作是不可逆的,即从A到B画一条线并不是简单的从B到A再画一条线。取消操作的唯一办法就是保存每一步操作的日志,在撤销时把记录过的操作(排除除最近一个操作)从头依次全部执行一遍。
/*接口定义*/var ReversibleCommand = new Interface('ReversibleCommand', ['execute']);/*命令对象类*/var MoveUp = function(cursor){ this.cursor = cursor;};MoveUp.prototype = { execute: function(){ this.cursor.move(0, -10); }};var MoveDown = function(cursor){ this.cursor = cursor;};MoveDown.prototype = { execute: function(){ this.cursor.move(0, 10); }};var MoveLeft = function(cursor){ this.cursor = cursor;};MoveLeft.prototype = { execute: function(){ this.cursor.move(-10, 0); }};var MoveRight = function(cursor){ this.cursor = cursor;};MoveRight.prototype = { execute: function(){ this.cursor.move(10, 0); }};var Undo = function(cursor){ this.cursor = cursor;};Undo.prototype = { execute: function(){ this.cursor.undo(); }}/*接收者类,实现具体的操作*/var Cursor = function(width, height, parent){ this.width = width; this.height = height; this.position = { x: width/2, y: height/2 }; this.commandStack = []; this.canvas = document.createElement('canvas'); this.canvas.width = this.width; this.canvas.height = this.height; parent.appendChild(this.canvas); this.ctx = this.canvas.getContext('2d'); this.ctx.fillStyle = '#CCC000'; this.move(0, 0);};Cursor.prototype = { move: function(x, y){ var _this = this; /*先记录整个命令*/ this.commandStack.push(function(){ _this.lineTo(x, y); }); /*再执行这个命令*/ _this.lineTo(x, y); }, lineTo: function(x, y){ this.ctx.save(); this.ctx.beginPath(); this.ctx.fillStyle = '#CCC000'; this.ctx.moveTo(this.position.x, this.position.y); this.position.x += x; this.position.y += y; this.ctx.lineTo(this.position.x, this.position.y); this.ctx.stroke(); this.ctx.closePath(); this.ctx.restore(); }, executeCommands: function(){ this.position = { x: this.width/2, y: this.height/2 }; this.ctx.clearRect(0, 0, this.width, this.height); for(var i=0; i
这里的undo操作也可以不用命令模式,只要像下面这样修改,感受一下:
/*1.修改 UndoButton类定义*/var UndoButton = function(label, parent, cursor){ /*这里没传入命令对象,不做接口检查,而是直接传入了接收者对象*/ this.element = document.createElement('button'); this.element.innerHTML = label; parent.appendChild(this.element); this.element.addEventListener('click', function(e){ cursor.undo();/*这里直接与接收者对象绑定,不是通过统一的execute方法对接*/ });};/*2.修改UndoButton实例化时传入的参数*/var undoButton = new UndoButton('Undo', body, cursor);/*3.去掉undo命令类的定义和实例化,也就是删掉下面的代码*/var Undo = function(cursor){ this.cursor = cursor;};Undo.prototype = { execute: function(){ this.cursor.undo(); }}var undoCommand = new Undo(cursor);
两个例子通用的html部分,只需要一个body标签
Document