接口模式中有三个参与者(三个对象):客户-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<len; i++){ if(typeof methods[i] !== 'string'){ throw new Error('Interface constructor expects method names to be passed in as a string'); } this.methods.push(methods[i]); } }; /*第一个参数为命令对象,从第二个参数起都为接口类的实例,表示对一个命令对象所实现的所有接口的检查*/ Interface.ensureImplements = function(object){ if(arguments.length < 2){ throw new Error('Function Interface.ensureImplements called with '+ arguments.length +' arguments but expected at least 2'); } for(var i=1,len=arguments.length; i<len; i++){ var interface = arguments[i]; if(interface.constructor !== Interface){ throw new Error('Function Interface.ensureImplements expects arguments two and above to be instance of Interface.'); } for(var j=0,methodsLen=interface.methods.length; j<methodsLen; j++){ var method = interface.methods[j]; if(!object[method] || typeof object[method] !== 'function'){ throw new Error('Function Interface.ensureImplements: object does not implement the ' + interface.name + ' interface. Method ' + method +' was not found.'); } } } }; ///*接口的使用方式*/ ///*1.创建一个接口*/ //var Person = new Interface('Person', ['getName', 'getAge']); ///*2.创建一个类实现上面的接口*/ //var Man = function(name, age){ // this.name = name; // this.age = age; //} //Man.prototype = { // getName: function(){ // return this.name; // }, //// getAge: function(){ //// return this.age; //// } //}; ///*3.使用前先测试接口实现*/ //var test = function(instance){ // Interface.ensureImplements(instance, Person); // var name = instance.getName(); // alert(name); //} //test(new Man('Hellen', 20));
第一个例子:
我们制作一个具有桌面应用程序风格的菜单栏,菜单栏中有多个菜单项,并通过使用命令对象,让这些菜单项执行各种操作。
/*接收者类,实现具体的操作*/ 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<this.commandStack.length; i++){ this.commandStack[i](); } }, undo: function(){ this.commandStack.pop();/*在命令栈中移除最近的一条*/ this.executeCommands();/*从头开始执行所有的命令*/ } }; /*用户界面类*/ var CommandButton = function(label, command, parent){ Interface.ensureImplements(command, ReversibleCommand);/*绑定命令对象前先检查接口*/ this.element = document.createElement('button'); this.element.innerHTML = label; parent.appendChild(this.element); /*通过在事件回调中调用命令对象的接口的方式来绑定命令对象*/ this.element.addEventListener('click', function(e){ command.execute(); }); }; var UndoButton = function(label, command, parent){ Interface.ensureImplements(command, ReversibleCommand); this.element = document.createElement('button'); this.element.innerHTML = label; parent.appendChild(this.element); this.element.addEventListener('click', function(e){ command.execute(); }); }; /*组合起来*/ var body = document.getElementsByTagName('body')[0]; var cursor = new Cursor(400, 400, body); var upCommand = new MoveUp(cursor); var downCommand = new MoveDown(cursor); var leftCommand = new MoveLeft(cursor); var rightCommand = new MoveRight(cursor); var undoCommand = new Undo(cursor); var upButton = new CommandButton('Up', upCommand, body); var downButton = new CommandButton('Down', downCommand, body); var leftButton = new CommandButton('Left', leftCommand, body); var rightButton = new CommandButton('right', rightCommand, body); var undoButton = new CommandButton('Undo', undoCommand, body);
这里的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标签
<!doctype html> <html> <head> <meta charset="UTF-8"> <meta name="Generator" content="EditPlus®"> <meta name="Author" content=""> <meta name="Keywords" content=""> <meta name="Description" content=""> <title>Document</title> </head> <body> </body> <script> </script> </html>