5分钟读书笔记之 - 设计模式 - 组合模式

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

5分钟读书笔记之 - 设计模式 - 组合模式

时间: 2024-10-14 00:16:14

5分钟读书笔记之 - 设计模式 - 组合模式的相关文章

5分钟读书笔记之 - 设计模式 - 门面模式

门面模式有俩个作用: 简化类的接口 消除类与使用它的客户代码之间的耦合 在javascript中,门面模式常常是开发人员最亲密的朋友.它是几乎所有javascript库的核心原则,门面模式可以使库提供的工具更容易理解.使用这种模式,程序员可以间接地与一个子系统打交道,与直接访问子系统相比,这样做更不容易出错. addEvent函数是一个基本的门面,你不用在每次为一个元素添加事件监听器的时候都得针对浏览器间的差异进行检查,有了这个便利,你可以把这个添加事件的底层细节抛在脑后,而把心思集中在如何构建

5分钟读书笔记之 - 设计模式 - 工厂模式

一个类或者对象中,往往会包含别的对象.在创建这种对象的时候,你可能习惯于使用常规方式,即用 new 关键字和类构造函数. 这会导致相关的俩个类之间产生依赖. 工厂模式,就是消除这俩个类之间的依赖性的一种模式,它使用一种方法来决定究竟实例化那个具体的类. 简单工厂模式 假设你想开几个自行车商店,每个商店都有几种型号的自行车出售,可以用这样一个类来表示: var BicycleShop = function(){} BicycleShop.prototype = { sellBicycle:func

5分钟读书笔记之 - 设计模式 - 单体模式

单体是一个用来划分命名空间,并将一批相关方法和属性组织在一起的对象,如果它可以被实例化,那么它只能被实例化一次. 单体模式,就是将代码组织为一个逻辑单元,这个逻辑单元中的代码可以通过单一的变量进行访问. 单体基本结构是这样: var Singleton = { attribute1:true, attribute2:10, method1:function(){}, method2:function(){} } 借助闭包实现单体: Namespace.Singleton = {} 定义之后立即执

5分钟读书笔记之 - 设计模式 - 桥接模式

补充一点知识: 私有变量 在对象内部使用'var'关键字来声明,而且它只能被私有函数和特权方法访问.私有函数 在对象的构造函数里声明(或者是通过var functionName=function(){...}来定义),它能被特权函数调用(包括对象的构造函数)和私有函数调用.特权方法 通过this.methodName=function(){...}来声明而且可能被对象外部的代码调用.可以使用:this.特权函数() 方式来调用特权函数,使用 :私有函数()方式来调用私有函数.公共属性 通过thi

5分钟读书笔记之 - 设计模式 - 命令模式

本章研究的是一种封装方法调用的方式.命令模式与普通函数有所不同.它可以用来对方法调用进行参数化处理和传送,经过这样处理过的方法调用可以在任何需要的时候执行. 它也可以用来消除调用操作的对象和实现操作的对象之间的耦合.这为各种具体的类的更换带来了极大的灵活性.这种模式可以用在许多不同的场合,不过它在创建用户界面这一方面非常有用,特别是在需要不受限的取消操作的时候.它还可以用来替代回调函数,因为它能够提高在对象之间传递的操作的模块化程度. 命令的结构: 最简形式的命令对象是一个操作和用以调用这个操作

5分钟读书笔记之 - 设计模式 - 装饰者模式

本章讨论的是一种为对象增添特性的技术,它并不使用创建新子类这种手段. 装饰者模式可以透明地把对象包装在具有同样接口的另一对象之中,这样一来,你可以给一些方法添加一些行为,然后将方法调用传递给原始对象.相对于创建子类来说,使用装饰者模式对象是一种更灵活的选择. 装饰者可用于为对象增加功能.它可以用来替代大量子类. 考虑前面的自行车类,你现在可能提供一些配件供用户选择,装饰者模式要求我们只需要创建选件类,这些类与四种自行车类都要实现Bicycle接口,但是他们只被用作这些自行车类的包装类.在这个例子

5分钟读书笔记之 - 设计模式 - 适配器模式

适配器模式可以用来在现在接口和不兼容的类之间进行适配. 使用这种模式的对象又叫包装器,因为他们是在用一个新接口包装另一个对象. 在设计类的时候往往遇到有些接口不能与现有api一同使用的情况,借助于适配器,你可以不用直接修改这些类也能使用他们. 适配器的特点: 适配器可以被添加到现有代码中以协调俩个不同的接口.从表面上来看,适配器模式很像门面模式,他们都要对别的对象进行包装并改变其呈现的接口,二者之间的差别在于他们如何改变接口.门面元素展现的是一个简化接口,它并不提供额外的选择,而且有时是为了方便

5分钟读书笔记之 - 设计模式 - 享元模式

本章探讨另一种优化模式-享元模式,它最适合于解决因创建大量类似对象而累及性能的问题.这种模式在javascript中尤其有用,因为复杂的javascript代码很快就会用光浏览器的所有可用内存,通过把大量独立对象转化为少量共享对象,可以降低运行web应用程序所需的资源数量. 享元模式用于减少应用程序所需对象的数量.这是通过对对象的内部状态划分为内在数据和外在数据俩类实现的.内在数据是指类的内部方法所需要的信息,没有这种数据的话类就不能正常运转.外在数据则是可以从类身上剥离并存储在其外部的信息.我

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

行为型:Command(命令模式) 命令模式: 目的:其实一般设计模式就是为了解耦.也没什么特别的,命令模式实际上就是将命令的请求者和命令的执行者解耦. 白话:领导说了,让把这个月的项目计划压缩到三个礼拜完成,还说了:"不管你用什么办法".这句“不管你用什么办法”就是我们所说的解耦.我不需要关心你怎么去做,我只要你能实现我想达到的目的. 模式结构:一般包含下面几个部分. Client:客户 Invoker:命令触发者 Command:命令 ConcreteCommand:具体命令实现