代码改变世界

读书笔记之 - javascript 设计模式 - 组合模式

2014-08-07 16:24  sai.zhao  阅读(143)  评论(0编辑  收藏  举报

组合模式是一种专为创建Web上的动态用户界面而量身定制的模式,使用这种模式,可以用一条命令在对各对象上激发复杂的或递归的行为。

在组合对象的层次体系中有俩种类型对象:叶对象和组合对象。这是一个递归定义,但这正是组合模式如此有用的原因所在。一个组合对象由一些别的组合对象和叶对象组成,其中只有叶对象不再包含子对象,叶对象是组合对象中最基本的元素,也是各种操作的落实地点。

  1. 存在一批组织成某种层次体系的对象(具体的结构在开发期间可能无法得知)
  2. 希望这批对象或其中的一部分对象实施一个操作

表单验证实例:

因为在开发期间无法得知要验证那些域,这正是组合模式可以大显身手的地方。我们逐一鉴别表单的各个组成元素,判断他们是组合对象还是叶对象。

我们不想为表单元素的每一种可能组合编写一套方法,而是决定让这俩个方法与表单域自身关联起来。也就是说,让每个域都知道如何保存和验证自己。

nameFieldset.validate();
nameFieldset.save();

这里的难点在于如何同时在所有域上执行这些操作。我们不想使用迭代结构的代码一一访问那些数目未知的域,而是打算用组合模式来简化代码,要保存所有的域,只需要这样一次调用即可:

topForm.save();

topForm 对象将在其所有子对象上递归调用save方法。实际上save操作只会发生在底层的对象上。组合对象只起到了一个传递调用的作用。

下面观赏组合对象的实现方法。

首先,创建那些组合对象和叶对象需要实现的俩个接口:

var Composite = new Interface('Composite',['add','remove','getChild']);
var FormItem = new Interface('FormItem',['save']);

CompositeForm 组合类的实现代码如下:

var CompositeForm = function(id,method,action){
    this.formComponents = [];
    this.element = document.createElement("form");
    this.element.id = id;
    this.element.method = method||'POST';
    this.element.action = action||'#';
}
CompositeForm.prototype.add = function(child){
    Interface.ensureImplements(child,Composite,FormItem);
    this.formComponents.push(child);
    this.element.appendChild(child.getElement());
}
CompositeForm.prototype.remove = function(child){
    for (var i=0,len=this.formComponents.length;i<len;i++) {
        if(this.formComponents[i]===child){
            this.formComponents.splice(i,1);
            break;
        }
    }
}
CompositeForm.prototype.save = function(){
    for (var i=0,len=this.formComponents.length;i<len;i++) {
        this.formComponents[i].save();
    }
}
CompositeForm.prototype.getElement = function(){
    return this.element;
}

CompositeForm 的子对象保存在一个数组对象中。这里实现的save方法显示了组合对象上的操作的工作方式:遍访各个子对象并对它们调用同样的方法。

现在看看叶对象类:

var Field =function(id){
    this.id = id;
    this.element;
}
Field.prototype.add = function(){};
Field.prototype.remove = function(){};
Field.prototype.getChild = function(){};
Field.prototype.save = function(){
    setCookie(this.id,this.getValue());
};
Field.prototype.getElement = function(){
    return this.element;
};
Field.prototype.getValue = function(){
    throw new Error('Unsupported operation on the class Field ');
};

这个类将被各个叶对象类继承。他将Composite接口中的方法实现为空函数,这是因为叶节点不会有子对象,你也可以让这几个函数抛出异常。

save方法用getValue方法获取所要保存的对象值,后一方法在各个子类中的实现各不相同。使用save方法,不用提交表单也能保存表单的内容。

var InputField = function(id,label){
    Field.call(this,id);

    this.input = document.createElement("input");
    this.input.id = id;

    this.label = document.createElement("label");
    var labelTextNode = document.createTextNode(label);
    this.label.appendChild(labelTextNode);

    this.element = document.createElement("div");
    this.element.className = 'input-field';
    this.element.appendChild(this.label);
    this.element.append(this.input);
}

extend(InputField,Field);
InputField.prototype.getValue = function(){
    return this.input.value;
}

InputField 是Field 的子类之一。它的大多数方法都是从Field继承而来,但他也实现了针对input标签的getValue方法的代码,TextareaField 和 SelectField 也实现了自己特有的getValue方法。

var TextareaField = function(id,label){
    Field.call(this,id);

    this.textarea = document.createElement("input");
    this.textarea.id = id;

    this.label = document.createElement("label");
    var labelTextNode = document.createTextNode(label);
    this.label.appendChild(labelTextNode);

    this.element = document.createElement("div");
    this.element.className = 'input-field';
    this.element.appendChild(this.label);
    this.element.append(this.textarea);
}

extend(TextareaField,Field);
InputField.prototype.getValue = function(){
    return this.textarea.value;
}
var SelectField = function(id,label){
    Field.call(this,id);

    this.select = document.createElement("select");
    this.select.id = id;

    this.label = document.createElement("label");
    var labelTextNode = document.createTextNode(label);
    this.label.appendChild(labelTextNode);

    this.element = document.createElement("div");
    this.element.className = 'input-field';
    this.element.appendChild(this.label);
    this.element.append(this.select);
}

extend(TextareaField,Field);
InputField.prototype.getValue = function(){
    return this.select.options[this.select.selectedIndex].value;
}

汇合起来,这就是组合模式大放光彩的地方,无论有多少表单域,对整个组合对象执行操作只需一个函数调用即可。

var contactForm = new CompositeForm('contact-form','POST','contact.php');
contactForm.add(new InputField('first-name','First Name'));
contactForm.add(new InputField('last-name','Last Name'));
contactForm.add(new InputField('address','Address'));
contactForm.add(new InputField('city','City'));
contactForm.add(new SelectField('state','STate',stateArray));
var stateArray = [{'a1','Alabama'}];
contactForm.add(new InputField('zip','Zip'));
contactForm.add(new TextareaField('zip','Zip'));

addEvent(window,'unload',contactForm.save);

可以把save的调用绑定到某个事件上,也可以用setInterval周期性的调用这个函数。

向FormItem添加新操作

首先,修改接口代码

var FormItem = new Interface('FormItem',['save','restore']);

然后是在叶对象类上实现这些操作。在本类中只要为超类Field添加这些操作,子类继承即可

Field.prototype.restore = function(){
    this.element.value = getCookie(this.id);
}

最后,为组合对象类添加相同的操作

CompositeForm.prototype.restore = function(){
    for (var i=0,len=this.formComponents.length;i<len;i++) {
        this.formComponents[i].restore();
    }
}

在实现中加入下面的这行代码就可以在窗口加载时恢复所有表单域的值。

addEvent(window,'load',contactForm.restore);

向层次体系中添加类

到目前为止只有一个组合对象类。如果设计目标要求对操作的调用有更多粒度上的控制,那么,可以添加更多层次的组合对象类,而不必改变其他类。假设我们想要对表单的部分执行save和restore操作,而不影响其他部分,一个解决办法是逐一在各个域上执行这些操作:

firstName.restore();
lastName.restore();

但是如果不知道表单具体会有那些域的情况下,这方法不管用。在层次体系中创建新的层次是一个更好的选择。我们可以把域组织在域集中,每一个域都是一个实现了FormItem接口的组合对象。在域集上调用restore将导致在其所有子对象上调用restore。

创建CompositeFieldset类并不需要为此类修改其他类,因为composite接口隐藏了所有内部实现细节,我们可以自由选用某种数据结构来存储子对象。作为示范,我们在此将使用一个对象来存储CompositeFieldset的子对象,而不像CompositeForm那样使用数组。

var CompositeFieldset = function(id,legendText){
    this.components = {};
    this.element = document.createElement("fieldset");
    this.element.id = id;

    if(legendText){
        this.legend = document.createElement("'legend'");
        this.legend.appendChild(document.createTextNode("legendText"));
        this.element.appendChild(this.legend);
    }
};
CompositeFieldset.prototype.add = function(child){
    Interface.ensureImplements(child,Composite,FormItem);
    this.components[child.getElement().id]=child;
    this.element.appendChild(child.getElement());
}

CompositeFieldset.prototype.remove = function(child){
    delete this.components[child.getElement().id];
}

CompositeFieldset.prototype.getChild = function(child){
    if(this.components[id]!=undefined){
        return this.components[id];
    }else{
        return null;
    }
}
CompositeFieldset.prototype.save = function(){
    for(var id in this.components){
        if(!this.components.hasOwnProperty(id)) continue;
        this.components[id].save();
    }
}
CompositeFieldset.prototype.restore = function(){
    for(var id in this.components){
        if(!this.components.hasOwnProperty(id)) continue;
        this.components[id].restore();
    }
}
CompositeFieldset.prototype.getElement = function(){
    return this.element;
}

CompositeFieldset 的内部细节与CompositeForm 截然不同,但是因为它与其他类实现了同样的接口,所以也能用在组合当中。只要对代码做少量修改即可获得这个功能。

var addressFieldset = new CompositeFieldset('address-fieldset');

addressFieldset.add(new InputField('address','Address'));
addressFieldset.add(new InputField('city','City'));
addressFieldset.add(new SelectField('state','STate',stateArray));
var stateArray = [{'a1','Alabama'},......];
addressFieldset.add(new InputField('zip','Zip'));
contactForm.add(addressFieldset);

contactForm.add(new TextareaField('comments','Comments'));
body.appendChild(contactForm.getElement());

addEvent(window,'unload',contactForm.save);
addEvent(window,'load',contactForm.restore);

addEvent('save-button','click',namefiledset.save);
addEvent('restore-button','click',namefiledset.restore);

现在我们使用域集对一部分域进行了组织。也可以直接把域加入到表单之中。(那个评语文本框就是一例),这是因为表单不在乎其子对象究竟是组合对象还是叶对象,只要他们实现了恰当的接口就行。在contactForm上执行的任何操作仍然会到达其所有子对象上。这样做的收获是获得了在表单的一个子集上执行这些操作的能力。

前面已经开了一个好头,用同样的方法还可以添加更多操作。可以为Field的构造函数增加一个参数,用以表明该域是否必须填写,然后基于这个属性实现一个验证方法。可以修改restore方法,以便在域没有保存过数据的情况下将其值设置为默认值,甚至还可以添加一个submit方法,用Ajax请求把所有的值发送打牌服务器端,由于使用了组合模式,添加这些操作并不需要知道表单具体是什么样子。

图片库示例:

在表单的例子中,由于HTML的限制,组合模式并没有充分利用。例如,你不能在表单中嵌套表单,而只能嵌套域集。真正的组合对象是可以内嵌在同类对象之中的。在下面这个实例中,任何位置都可以换用任何对象。

这次的任务是创建一个图片库。我们希望能有选择地隐藏或者显示图片库的特定部分。这可能是单独的图片,也可能是图片库。其他操作以后还可以添加,现在我们只关注hide和show操作。需要的类只有俩个:用作图片库的组合对象类和用于图片本身的叶对象类。

var Composite = new Interface('Composite',['add','remove','getChild']);
var GalleryItem = new Interface('GalleryItem',['hide','show']);

var DynamicGallery = function(id){
    this.children=[];
    this.element = document.createElement("div");
    this.element.id = id;
    this.element.className = 'dynamic-gallery';
}
DynamicGallery.prototype = {
    constructor:DynamicGallery,
    add:function(child){
        Interface.ensureImplements(child,Composite,GalleryItem);
        this.children.push(child);
        this.element.appendChild(child.getElement());
    },
    remove:function(child){
        for (var node,i=0;node=this.getChild(i);i++) {
            if(node==child){
                this.children.splice(i,1);
                break;
            }
        }
        this.element.removeChild(child.getElement());
    },
    getChild:function(i){
        return this.children[i];
    },
    //  GalleryItem的接口
    hide:function(){
        for (var node,i=0;node=this.getChild(i);i++) {
            node.hide();
        }
        this.element.style.display = "none";
    },
    show:function(){
        this.element.style.display = "block";
        for (var node,i=0;node=this.getChild(i);i++) {
            node.show();
        }
    },
    //辅助方法
    getElement:function(){
        return this.element;
    }
}

在上面的代码中,首先定义的是组合对象类和叶对象类应该实现的接口,除了常规的组合对象方法外,这些类要定义的操作值只包括hide和show。接下来定义的是组合对象类,由于DynamicGallery只是对div元素的包装,所以图片库可以再嵌套图片库,而我们因此也就只需要用到一个组合对象类。

叶节点也非常简单,他是对image元素的包装,并且实现了hide和show方法:

var GalleryImage = function(src){
    this.element = document.createElement("img");
    this.element.className = 'gallery-image';
    this.element.src = src;
}
GalleryImage.prototype = {
    add:function(){},
    remove:function(){},
    getChild:function(){},
    hide:function(){
        this.element.style.display = '';
    },
    show:function(){
        this.element.style.display='block';
    },
    getElement:function(){
        return this.element;
    }
}

这是一个演示组合模式的工作方式的好例子。每个类都很简单,但由于有了这样一种层次体系,我们就可以执行一席复杂的操作。GalleryImage类的构造函数会创建一个image元素,这个类定义中的其余部分由空的组合对象方法和GalleryItem要求的操作组成。现在我们可以使用类来管理图片。

var topGallery = new DynamicGallery('top-gallery');
topGallery.add(new GalleryImage('./images/image-1.jpg'));
topGallery.add(new GalleryImage('./images/image-2.jpg'));
topGallery.add(new GalleryImage('./images/image-3.jpg'));

var vacationPhotos = new DynamicGallery('vacation-photos');
for (var i=0;i<30;i++) {
    vacationPhotos.add(new GalleryImage('/images/vac/image-'+i+'.jpg'));
}
topGallery.add(vacationPhotos);
topGallery.show();
vacationPhotos.hide();

在组织图片的时候,DynamicGallery这个组合对象类你想用多少次都行。因为DynamicGallery实例化的组合对象可以嵌套在同类对象中,所以只用这俩各类的实例就能建造出任意大的层次体系。

使用组合模式,简单的操作也能产生复杂的结果,你不必编写大量的手工遍历数组或其他数据结构的粘合代码,只需要对最顶层的对象执行操作,让每一个子对象自己传递这个操作即可。这对那些再三执行的操作尤其有用。

在组合模式对象中,各个对象之间的耦合非常松散,只要他们实现了同样的接口,那么改变他们的位置或互换他们只是举手之劳。这促进了代码的重用,也有利于代码重构。

用组合模式组织起来的对象形成了一个出色的层次体系。每当对顶层组合对象执行一个操作时,实际上是在对整个结构进行深度优先的搜索以查找节点。而创建组合对象的程序员对这些细节一无所知。在这个层次体系中添加、删除和查找节点都非常容易。

组合对象的弊端:

组合对象的易用性可能掩盖了它所支持的每一种操作的代价。由于对组合对象调用的任何操作都会被传递到它的所有子对象,如果这个层次体系很大的话,系统的性能将会受到影响。程序员可能一时还不会察觉到。像topGallery.show()这样一个方法调用会引起一次对整个树结构的遍访,尽量在文档中说明这个情况。

小结:如果运用得当,那么组合模式是一种非常管用的模式。它把一批子对象组织为树形结构,只要一条命令就可以操作树中的所有对象。这种模式特别适合于动态的HTML用户界面。在它的帮助下,你可以在不知道用户界面的最终格局的情况下进行开发。对于每一个javascript程序员,它都是最有用的模式之一。