通过两个例子来理解js中的命令模式

接口模式中有三个参与者(三个对象):客户-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>


你可能感兴趣的:(js命令模式)