组合模式是一种专为创建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程序员,它都是最有用的模式之一。

读书笔记之 - javascript 设计模式 - 组合模式的更多相关文章

  1. 读书笔记之 - javascript 设计模式 - 命令模式

    本章研究的是一种封装方法调用的方式.命令模式与普通函数有所不同.它可以用来对方法调用进行参数化处理和传送,经过这样处理过的方法调用可以在任何需要的时候执行. 它也可以用来消除调用操作的对象和实现操作的 ...

  2. 读书笔记之 - javascript 设计模式 - 门面模式

    门面模式有俩个作用: 简化类的接口 消除类与使用它的客户代码之间的耦合 在javascript中,门面模式常常是开发人员最亲密的朋友.它是几乎所有javascript库的核心原则,门面模式可以使库提供 ...

  3. 读书笔记之 - javascript 设计模式 - 工厂模式

    一个类或者对象中,往往会包含别的对象.在创建这种对象的时候,你可能习惯于使用常规方式,即用 new 关键字和类构造函数. 这会导致相关的俩个类之间产生依赖. 工厂模式,就是消除这俩个类之间的依赖性的一 ...

  4. 读书笔记之 - javascript 设计模式 - 代理模式

    代理(proxy)是一个对象,它可以用来控制对另一对象的访问.它与另外那个对象实现了同样的接口,并且会把任何方法调用传递给那个对象.另外那个对象通常称为本体.代理可以代替本体被实例化,并使其可被远程访 ...

  5. 读书笔记之 - javascript 设计模式 - 单体模式

    单体是一个用来划分命名空间,并将一批相关方法和属性组织在一起的对象,如果它可以被实例化,那么它只能被实例化一次. 单体模式,就是将代码组织为一个逻辑单元,这个逻辑单元中的代码可以通过单一的变量进行访问 ...

  6. 读书笔记之 - javascript 设计模式 - 享元模式

    本章探讨另一种优化模式-享元模式,它最适合于解决因创建大量类似对象而累及性能的问题.这种模式在javascript中尤其有用,因为复杂的javascript代码很快就会用光浏览器的所有可用内存,通过把 ...

  7. 读书笔记之 - javascript 设计模式 - 责任链模式

    责任链模式可以用来消除请求的发送者和接收者之间的耦合.这是通过实现一个由隐式地对请求进行处理的对象组成的链而做到的.链中的每个对象可以处理请求,也可以将其传给下一个对象. 责任链的结构: 责任链由多个 ...

  8. 读书笔记之 - javascript 设计模式 - 装饰者模式

    本章讨论的是一种为对象增添特性的技术,它并不使用创建新子类这种手段. 装饰者模式可以透明地把对象包装在具有同样接口的另一对象之中,这样一来,你可以给一些方法添加一些行为,然后将方法调用传递给原始对象. ...

  9. javascript设计模式-组合模式

    组合模式所要解决的问题: 可以使用简单的对象组合成复杂的对象,而这个复杂对象有可以组合成更大的对象.可以把简单这些对象定义成类,然后定义一些容器类来存储这些简单对象. 客户端代码必须区别对象简单对象和 ...

随机推荐

  1. Android 布局之DrawLayout

    在刚开始学android的时候肯定会知道,android的主要的布局就是LinearLayout.RelativeLayout.FramLayout.AbsoluteLayout.以及TableLay ...

  2. codeforces --- 115A

    A. Party time limit per test 3 seconds memory limit per test 256 megabytes input standard input outp ...

  3. 【转】 std list/vector sort 排序

    [转自]http://blog.csdn.net/marising/article/details/4567531 网上江湖郎中和蒙古大夫很多,因此,此类帖子也很多.关于排序,我还真没研究过,看了江湖 ...

  4. HDOJ(HDU) 2523 SORT AGAIN(推导排序、、)

    Problem Description 给你N个整数,x1,x2-xn,任取两个整数组合得到|xi-xj|,(0 < i,j<=N,i!=j). 现在请你计算第K大的组合数是哪个(一个组合 ...

  5. 神器 Sublime Text 3 的一些常用插件

    ConvertToUTF8 支持UTF-8编码的插件 Bracket Highlighter 用于匹配括号,引号和html标签.对于很长的代码很有用.安装好之后,不需要设置插件会自动生效 DocBlo ...

  6. wiki 的捐款呼吁有感而发

    今早在维基百科上查东西时,网页的最上角出现了一栏小广告,我一看居然是维基创始人发的捐款呼吁,点开后网页内容如下: “     来自维基百科创办者Jimmy Wales的个人呼吁 ----------- ...

  7. java_method_readFile读取文件文本txt

    /** * @Title: TxtAndCsvUtils.java * @Package cn.com.qmhd.tools * @Description: TODO(读取txt和CSV文档) * @ ...

  8. c#语言基础编程—string

    引言 在c#中经常会有相关的string的操作,string类型为引用类型,集成于Object,所以会有四个方法.详情可见 值类型和引用类型的区别 里面详细介绍了,值类型和引用类型的区别和应用场合,所 ...

  9. Tomcat配置多个端口号或多个应用

    一.在Tomcat下配置一个应用服务(service)中,配置多个端口号. 即一个service配置多个端口,项目可以通过多个端口访问. 修改tomcat-home\conf下的server.xml, ...

  10. Python:urllib和urllib2的区别(转)

    原文链接:http://www.cnblogs.com/yuxc/ 作为一个Python菜鸟,之前一直懵懂于urllib和urllib2,以为2是1的升级版.今天看到老外写的一篇<Python: ...