有人好奇JAse中的undo和redo是怎么做的,并且给出自己的设计做法也各有特色。我最初采用的是全文保存方法(这一方法也是最简单有效、使用最广泛的),后来被人痛批一顿换成了命令链。命令链其实是基于一种叫做“命令”的设计模式的,关键思想在于面向接口编程。这个东西随便搜搜有很多例子,我再重复制造一下轮子吧。
AS3中的命令链
as3对OOP的支持已经比较完善,所以可以据此写出很好的命令链,先从简单的例子来说起(以下代码均被简化)。假如我们要做一个Dog类,Dog的“说话”方式是bark(狗叫):
class Dog {
public function bark():void {
trace("汪汪!");
}
}
这很简单,没有什么特殊之处。可是奇怪的事情发生了,中学英语课本中有个闻名的澳洲野狗Dingo,它的叫声和普通的狗不一样:
class Dingo extends Dog {
public override function bark():void {
trace("呜呜——");
}
}
也很简单,Dingo毕竟还是一只狗,只需继承并覆盖即可。好了现在需求来了:我们想听听这些动物的叫声是什么样的,只需要调用下相应的方法即可。
public class Test {
public function Test():void {
var dog:Dog = new Dog();
dog.bark();
var dingo:Dog = new Dingo();
dingo.bark();
}
}
目前为止一切顺利,可惜未来总是不像我们想象的那样。动物中又多了只公鸡,它的叫法完全不一样,是打鸣。
class Cock {
public function crow():void {
trace("喔喔——");
}
}
这下坏了,我们在听叫声的时候得记住,公鸡的叫法和狗不一样:
public class Test {
public function Test():void {
var dog:Dog = new Dog();
dog.bark();
var dingo:Dog = new Dingo();
dingo.bark();
var cock:Cock = new Cock();
cock.crow();
}
}
倘若把这些动物排成一列,让它们依次各自叫一下的话就更麻烦了:
public class Test {
public function Test():void {
var list:Array = [new Dog(), new Dingo(), new Cock()];
for (var i:int = 0; i < list.length; i++) {
var item = list[i];
if (item is Dog) {
item.bark();
}
else if (item is Cock) {
item.crow();
}
}
}
}
我们得对每种类型做单独判断,可所谓不胜其烦,动物的种类是不可预知的,倘若再来一只猫,还要继续增加判别吗?当然不能。
显然,关于叫法这里我们需要解耦。不管你是什么动物,这些叫声其实都可以概括为“说话”(动物们也有自己的语言和说话方式)。在主调程序中,我们并不想关心动物是“怎么说话”、“说些什么”的,我们只想调用一个命令,动物对象就能自动按照自己的方式来“说话”,甚至我们不用关心这个动物到底是什么。通过分离做什么和怎么做来实现这个目标的方式,就称为命令模式。
好了,首先是命令接口:
public interface Command {
function say():void;
}
所有动物只要实现了这个接口就行,各自具体的执行方式自己来定:
class Dog implements Commad {
public function say():void {
bark();
}
protected function bark():void {
trace("汪汪!");
}
}
class Dingo extends Dog {
protected override function bark():void {
trace("呜呜——"");
}
}
class Cock implements Commad {
public function say():void {
crow();
}
private function crow():void {
trace("喔喔——");
}
}
这样的话,在主调程序中,管它排成一列的动物有哪些,统统视作Command接口的实现即可:
public class Test {
public function Test():void {
var list:Array = [new Dog(), new Dingo(), new Cock()];
for (var i:int = 0; i < list.length; i++) {
(list[i] as Command).say();
}
}
}
哪怕再多出来一只猫,我们只需要增加猫的这一类别,列表中多出只猫来即可,主调程序无需关心具体实现。甚至猫的方式更加复杂有感情:
class Cat implements Command {
private var isHappy:Boolean = true;
public function say():void {
if (isHappy) {
purr();
}
else {
mew();
}
}
private function purr():void {
trace("咕噜……");
}
private function mew():void {
trace("喵——");
}
}
public class Test {
public function Test():void {
var list:Array = [new Dog(), new Dingo(), new Cock(), new Cat()];
for (var i:int = 0; i < list.length; i++) {
(list[i] as Command).say();
}
}
}
undo、redo的命令链
熟悉了这些后,undo和redo的做法同理:只需实现了命令接口,每个操作各是独自的命令,互不干涉,具体实现自己决定,最大程度上解耦。
定义命令接口(以下代码均被简化):
package command {
public interface ICommand {
function redo():void;
function undo():void;
}
}
输入命令——也就是在编辑器中敲入代码时:
package command {
import flash.text.*;
public class InputCommand implements ICommand {
private var tf:TextField;
private var index:int;
private var text:String;
public function InputCommand(tf:TextField, index:int, text:String):void {
this.tf = tf;
this.index = index;
this.text = text;
}
public function redo():void {
tf.setSelection(index + text.length, index + text.length);
}
public function undo():void {
tf.replaceText(index, index + text.length, "");
}
}
}
删除命令——当按Delete键将编辑器里的内容删除时:
package command {
import flash.text.*;
public class DeleteCommand implements ICommand {
private var tf:TextField;
private var index:int;
private var end:int;
private var text:String;
public function DeleteCommand(tf:TextField, index:int, end:int, text:String):void {
this.tf = tf;
this.index = index;
this.end = end;
this.text = text;
}
public function redo():void {
tf.replaceText(index, index + text.length, "");
}
public function undo():void {
tf.replaceText(index, index, text);
}
}
}
于是,每当我们输入一个字符时,向一个记录命令步骤的数组里存入一个输入命令;而在按下Delete键时,存入一个删除命令;其它多一个命令多建一个类实现,这样一个链连下来就是命令链。
package command {
import flash.text.*;
import edit.*;
public class CommandList {
private var undoList:Array, redoList:Array;
public function CommandList():void {
clear();
}
public function addCommand(cmd:ICommand):void {
//超过最大命令链长度需先出队列一个
if (undoList.length > Editor.UNDO_SIZE) {
undoList.shift();
}
//每添加一次命令,清空redoList
if(redoList.length) {
redoList = new Array();
}
undoList.push(cmd);
}
public function undo():Boolean {
//undoList中有命令则执行,并将相应命令出栈存入redoList中
if(undoList.length) {
var cmd:ICommand = undoList.pop() as ICommand;
cmd.undo();
redoList.push(cmd);
return true;
}
//为空返回false
else {
return false;
}
}
public function redo():Boolean {
//redoList中有命令则执行,并将相应命令出栈存入undoList中
if (redoList.length) {
var cmd:ICommand = redoList.pop() as ICommand;
cmd.redo();
undoList.push(cmd);
return true;
}
//为空返回false
else {
return false;
}
}
public function clear():void {
undoList = new Array();
redoList = new Array();
}
}
}
其实基于fp10中as3的新特性,有个叫Vector的类,它是存储命令链数组的更好的替代者,因为它规定数组里所有元素的类型必须是相同的。这样在编译期间便能防止出错,于是undoList和redoList更好的定义方式是这样:
undoList = new Vector.<ICommand>(); redoList = new Vector.<ICommand>();
熟悉Java的很容易理解:这就是Java5+中的泛型。
js中的命令链
扯了这么远才到js,而且篇幅也不会多长,真是愧对这标题。
js中并不存在接口,所以也就无从说起面向接口编程。然而以此就下定论说“不能使用命令链”尚为时过早。的确,js由于其本身特性原因,不能像传统OOP语言那样使用,但命令链模式其实就存在于日常生活当中:
function Dog() {
}
Dog.prototype.say = function() {
this.bark();
}
Dog.prototype.bark = function() {
alert("汪汪!");
}
function Dingo() {
}
Dingo.prototype = new Dog();
Dingo.prototype.bark = function() {
alert("呜呜——");
}
function Cock() {
}
Cock.prototype.say = function() {
this.crow();
}
Cock.prototype.crow = function() {
alert("喔喔——");
}
function Cat() {
this.bHappy = true;
}
Cat.prototype.say = function() {
if(this.bHappy) {
this.purr();
}
else {
this.mew();
}
}
Cat.prototype.purr = function() {
alert("咕噜……");
}
Cat.prototype.mew = function() {
alert("喵——");
}
var aList = [new Dog(), new Dingo(), new Cock(), new Cat()];
for(var i = 0; i < aList.length; i++) {
aList[i].say();
}
基于弱类型,每个类都可以拥有一个同名方法,广义上说这也可以看作是实现了一个“通用接口”。js没有编译过程,因此无法在前期(编译期)实现检查,只能靠后期(运行时)来确定。网上也有很多例子模拟出js的接口和命令链,使得一个类在实现接口但却没有实现接口定义的方法时抛出异常。这样做的好处是在运行出错时有详尽的异常信息供使用者检查,但在目前firebug等工具的情况下所提供的能效有限,在大型的开发中才会有很好的帮助。因此我们目前大部分情况下——似乎最好的办法就是相信别人。

支持! 谢谢.
看了两天也没敢回复.
今天还是不敢.呵呵..
@云墨雪: 那你现在在干什么……
This is my first visit here, but I will be back soon, because I really like the way you are writing, it is so simple and honest
It is useful to try everything in practice anyway and I like that here it’s always possible to find something new.