使用设计模式的要诀时把它们看作你编程工具箱中的多种工具,每一种都有一个特定的用途。在尝试应用设计模式至代码之前,你首先要熟悉各种现有的设计模式以及每一种所应用的场合(应用了错误的工具将导致不必要的麻烦和时间的浪费)。这些章节将帮助你来实现这一点。除非你是一个特别老练的JavaScript开发者,不然,在你开始为应用程序编写代码时头脑是没有特定的设计模式的。随着代码的不断积累,你将发现需要对代码进行改变以使日后的开发更易于管理,同时还要对代码库中的文件按照一定的结构和规律进行管理。此过程通常称为“重构”。通常,就是在开发的此阶段要考虑应用特定的设计模式或架构模式至代码,以简化后续的开发工作。要谨慎对待那些坚持要在项目启动时就要在头脑中保持某个特定的设计模式来开展编码工作或在开始时就使用某种特定的预建的JavaScript框架的人员。原因是,除非他们是非常有经验的专业人员,不然,这等同于在能识别出解决问题所需使用的工具之前就直接选用一种全新的没有使用经验的工具。
你应该熟练掌握这里每一章所介绍的设计模式和架构模式,仔细学习每一种模式并领会其使用的方式方法。随着时间的积累,你会将逐渐识别出代码所需要应用的特定模式,进而改进代码的可维护性,并在某种情况下提升效率。
5.1 什么是设计模式
设计模式时经过尝试和测试,通过验证,通过验证的代码编写和组织方法。通过开发者提供清晰的结构,移除不必要的复杂性,并将大型代码库中的不同部分的连接进行解耦 ,使得代码更易于理解,易于维护。各种设计模式就好比编程工具箱中的各种工具。
介绍23种不同的设计模式,这些模式可划分为3大类:创建型,结构型和行为型。
5.2 创建型设计模式
创建型设计模式为你描述了用于创建对象的“类”或方法,而不需要你自己直接创建对象。在决定何对象或何种对象的类型才是与你所面对的特定情况和需求最为相关的时,此抽象层给了你以及你的代码以更高的灵活性。在这里,我将为你介绍5种创建型设计模式,每种设计模式都配有相应的例子。你会发现这些设计模式对你的代码非常有用。
5.21 工厂模式
利用工厂模式,可以实现在不指定特定的“类”而创建出对象。在之前的章节,当涉及“类”时,我们需要直接使用JavaScript关键字new来创建某特定“类”或子类的实例。而利用工厂模式,对象的创建处理过程被予以抽象,使得相对复杂的对象创建处理过程得以封装于简单的接口之下,而不需要使用new关键字。此抽象意味着,用作创建各实例的后台“类”的类型和方法可以随时被完全替换,而不需要改变接口来适应类的创建。从其他开发者角度来说,并不需要例会接口底下所发生的变化。如果预知到在将来可能需要作出许多更改,但又不希望必须重写散步在大量其他代码中的“类”的实例化代码,则使用工厂模式是很理想的做法。
代码清单5-1显示了工厂模式的例子。当中根据工厂方法的不同输入参数,实现了基于若干不同的“类”进行对象的实例化。
代码清单5-1 工厂模式
1 //定义一个工厂,它会基于所输入的内容,使用最适合的“类”来为我们创建出相应的表单域对象 2 var FormFieldFactory={ 3 //makeField方法使用一下2个选项 4 // -type,定义所创建的表单域对象类型,例如text,email或button 5 // -displayText,基于对象的类型,定义表单域的placeholder(占位符)文本或按钮上所显示的文本 6 makeField:function(options){ 7 var options=options ||{}, 8 type=options.type||"text", 9 displayText=options.displayText ||"",10 field;11 //基于所提供的类型使用最适合的“类”来创建对象实例12 switch (type){13 case "text":14 field=new TextField(displayText);15 break;16 case "email":17 field=new EmailField(displayText);18 break;19 case "button":20 field=new ButtonField(displayText);21 break;22 //如果不确定,则使用TextField“类”23 default:24 field=new TextField(displayText);25 break;26 }27 return field;28 29 }30 };31 //定义TextField“类”,用于创建表单元素32 function TextField(displayText){33 this.displayText=displayText;34 }35 36 //getElement方法将利用所提供的placeholder文本值来创建一个DOM元素37 TextField.prototype.getElement=function(){38 var textField=document.createElement("input");39 textField.setAttribute("type","text");40 textField.setAttribute("placeholder",this.displayText);41 return textField;42 }43 44 //定义EmailField类,用于创建表单元素45 function EmailField(displayText){46 this.displayText=displayText;47 }48 //getElement方法将利用所提供的placeholder文本值来创建一个DOM元素49 EmailField.prototype.getElement=function(){50 var emailField=document.createElement("input");51 emailField.setAttribute("type","email");52 emailField.setAttribute("placeholder",this.displayText);53 return emailField;54 };55 56 //定义ButtonField“类”,用于创建
代码清单5-2演示了如何在应用程序中使用代码清单5-1所创建的工厂。
代码清单5-2 使用工厂模式
1 //使用工厂来创建一个文本输入表单域,一个email表单域和一个提交按钮。请留意我们如何在不需要知道 2 //底层的那些“类”或它们的特定输入的情况下创建表单域的。FormFieldFactory对该方式进行了抽象 3 var textField=FormFieldFactory.makeField({type:"text",displayText:"Enter the first line of your address"}), 4 emailField=FormFieldFactory.makeField({type:"email",displayText:"Enter your email address"}), 5 buttonField=FormFieldFactory.makeField({type:"button",displayText:"Submit"}); 6 7 //等到浏览器的load事件触发后,把由这三个新创建的对象所表示的DOM元素添加至当前页面 8 window.addEventListener("load",function(){ 9 var bodyElement=document.body;10 11 //使用每个对象的getElement()方法,获得对其DOM元素的引用,以将其添加至页面12 bodyElement.appendChild(textField.getElement());13 bodyElement.appendChild(emailField.getElement());14 bodyElement.appendChild(buttonField.getElement());15 },false);
在需要在代码的其余所有部分通过屏蔽较为复杂的对象创建方法来简化某些特定对象的创建过程时,使用工厂模式最适合不过了。
http://www.cnblogs.com/myzy/p/5240457.html
5.2.2 抽象工厂模式
抽象工厂(Abtract Factory)模式比起我们刚刚见识的工厂模式又更进了一步。如果应用程序需要,它可以创建出一个额外的抽象层,根据共同的用途或主题来一起创建出多个工厂。代码清单5-3中的代码演示了这个模式。它对代码清单5-1做了扩展,将两个工厂作为一个新的工厂类型的实例来对待,这两个工厂享有相似的行为。
代码清单5-3 抽象工厂模式
1 //定义一个基础工厂“类”、于创建表单域,其他更明确的表单域创建工厂“类”将继承于此类 2 function FormFieldFactory() { 3 4 //定义所支持的表单域类型的清单,它们将会应用于所有的继承于此类的表单域工厂类 5 this.availableTypes = { 6 TEXT: "text", 7 EMAIL: "email", 8 BUTTON: "button" 9 }; 10 } 11 FormFieldFactory.prototype = { 12 13 //定义makeField()方法,它将被个子类利用多态性进行重写。因此,该方法不应该在此父“类”中直接调用 14 //如果出现这种情况,则抛出一个错误 15 makeField: function() { 16 throw new Error("This method should not the be called directly."); 17 } 18 }; 19 20 //定义一个工厂“类”,继承于基础工厂“类”,用于HTML5表单的创建 21 //更要详细地了解这些表单域域HTML4的表单域的不同之处 22 function Html5FormFieldFactory() {} 23 Html5FormFieldFactory.prototype = new FormFieldFactory(); 24 25 //针对此工厂使用明确的代码来重写makeField()方法 26 Html5FormFieldFactory.prototype.makeField = function(options) { 27 var options = options || {}, 28 type = options.type || this.availableTypes.TEXT, 29 displayText = options.displayText || "", 30 field; 31 32 //基于所提供的options,选择最合适的域类型 33 switch(type) { 34 case this.availableTypes.TEXT: 35 field = new Html5TextField(displayText); 36 break; 37 case this.availableTypes.EMAIL: 38 field = new Html5EmailField(displayText); 39 break; 40 case this.availableTypes.BUTTON: 41 field = new ButtonField(displayText); 42 break; 43 default: 44 throw new Error("Invalid field type specified:" + type); 45 break; 46 } 47 return field; 48 }; 49 50 //定义一个工厂“类”,它也继承于相同的基础工厂“类”,用于老式HTML4表单域的创建 51 function Html4FormFieldFactory() {} 52 Html4FormFieldFactory.prototype = new FormFieldFactory(); 53 54 //针对此工厂,使用明确的代码来重写makeField()方法 55 Html4FormFieldFactory.prototype.makeField = function(options) { 56 var options = options || {}, 57 type = options.type || this.availableTypes.TEXT, 58 displayText = options.displayText || "", 59 field; 60 61 //基于所提供的options,选择最合适的域类型 62 switch(type) { 63 case this.availableTypes.TEXT: 64 case this.availableTypes.EMAIL: 65 field = new Html4TextField(displayText); 66 break; 67 case this.availableTypes.BUTTON: 68 field = new ButtonField(displayText); 69 break; 70 default: 71 throw new Error("Invalid field type specified:" + type); 72 break; 73 } 74 return field; 75 } 76 77 //定义各项表单域“类”,用于创建各种HTML5和HTML4表单元素 78 function Html5TextField(displayText){ 79 this.displayText=displayText||""; 80 } 81 82 Html5TextField.prototype.getElement=function(){ 83 var textField=document.createElement("input"); 84 textField.setAttribute("type","text"); 85 textField.setAttribute("placeholder",this.displayText); 86 return textField; 87 } 88 //因为HTML4并不支持placeholder标签特性,作为代替,我们将创建并返回一个元素,当中包含着文本域 89 //和一个相关联的包含着placeholder文本90 function Html4TextField(displayText){ 91 this.displayText=displayText; 92 } 93 Html4TextField.prototype.getElement=function(){ 94 var wrapper=document.createElement("div"), 95 textField=document.createElement("input"), 96 textFieldId="text-field"+Math.floor(Math.random()*999), 97 label=document.createElement("label"), 98 labelText=document.createTextNode(this.displayText); 99 100 textFiled.setAttribute("type","text");101 textField.setAttribute("id",textFieldId);102 103 //使用label的for标签特性与input的id标签特性把该
我们可以如代码清单5-4所示般使用代码清单5-3中的抽象工厂,基于对运行代码所在浏览器的支持情况。生成合适类型的各种表单元素。
代码清单5-4 使用抽象工厂模式
1 //确认浏览器是否支持HTML5,并选择合适的表单域工厂 2 var supportsHtml5Fields = (function() { 3 4 //此自执行函数尝试创建一个HTML5表单域类型的元素 5 var field = document.createElement("input"); 6 field.setAttribute("type", "email"); 7 //如果该新表单域返回了正确的域类型,那就代表它已经被正确地创建了,也就代表着该浏览器支持HTML5。 8 //否则就说明只支持HTML4 9 return field.type === "email";10 }()),11 //利用上述变量所返回的值来选择合适的表单域的创建工厂“类”,并使用该“类”创建一个实例12 formFieldFactory = supportsHtml5Fields ? new Html5FormFieldFactory() : new Html4FormFieldFactory(),13 //使用该工厂来创建一个文本框表单域,一个email表单域和一个提交按钮。此时,我们已经是根据当前14 //浏览器的情况使用了最适合的域类型和标签属性来进行创建的了15 textField = formFieldFactory.makeField({16 type: "text",17 displayText: "Enter the first line of your address"18 }),19 emailField = formFieldFactory.makeField({20 type: "email",21 displayText: "Enter your email addrss"22 }),23 24 //请留意我们是如何利用含有工厂“类”所支持的域类型清单的availableTypes属性而不是使用硬编码的文本字符串来设定表单域类型的。25 //推荐使用这种方法,因为使用变量要优于使用硬编码值,可降低日后的维护成本26 buttonField = formFieldFactory.makeField({27 type: formFieldFactory.availableTypes.BUTTON,28 displayText: "Submit"29 });30 //等到浏览器的load事件触发后,便将由这3个新创建的对象所表示的DOM元素添加至当前页面31 window.addEventListener("load",function(){32 var bodyElement=document.body;33 //使用每个对象的getElement()方法来获取对它的DOM元素的引用,以便将其添加至页面34 bodyElement.appendChild(textField.getElement());35 bodyElement.appendChild(emailField.getElement());36 bodyElement.appendChild(buttonField.getElement());37 },false);
当需要从现有代码中的多个“类”中,根据这些“类”之间公有的目的或通过的主题,创建出一个额外的抽象层,以降低应用程序的其余开发工作的复杂性时,使用抽象工厂模式最为适合。
5.2.3 生成器模式
学习过工厂和抽象工厂模式后我们发现,生成器(Builder)模式抽象了对象的创建过程。在此模式中,我们只需要提供我们所希望创建的对象的内容和类型即可,而决定使用哪个“类”来进行对象创建的处理工作则由生成器抽象了出来。通过把创建过程划分为一系列的较小步骤,便可以有效地完成一个完整对象的创建。最后调用一个操作来“生成”预期对象,并将其返回给发出调用的代码。一个生成器中可以潜在性地包含大量代码,应用所有的这些内容的明确目的是为了让开发人员尽可能轻松地进行对象创建。
代码清单5-5展示的就是生成器模式,当中定义了一个生成器,用于创建简单的HTML表单。表单可以包含按各种顺序、在任意时刻添加的任意数量的各种类型表单域。一旦所有的表单域添加完毕,使用getForm()“生成”方法便可以需要使用表单的时候生成并返回该<form>元素。
代码清单5-5 生成器模式
1 //定义一个生成器“类”,用于构建简单的表单元素。此表单元素可以根据终端开发者的需要进行配置。 2 //终端开发者将实例化该生成器,并根据应用程序整个运作过程的需要,把各项表单域添加至该表单元素, 3 //最后,调用一个方法来返回一个包含着所有添加的表单域的
代码清单5-5中的表单生成器现在可以再应用程序中使用了,如代码清单5-6所示。我们可以添加一些表单域至表单中,而不必直接去实例化任何表单或表单域的“类”,也不需要手动地去创建任何DOM元素。最后的对象是通过使用getForm()方法“创建”出来的,它被返回至调用getForm()方法的代码中。
代码清单5-6 使用生成器模式
1 //实例化表单生成器 2 var formBuilder=new FormBuilder(), 3 form; 4 //在应用程序中,以任意顺序,在任意时间都可以添加表单域,所需要的只是类型和内容 5 //实际上的对象创建已经在生成器内进行了抽象 6 formBuilder.addField("text","Enter the first line of your address"); 7 formBuilder.addField("email","Enter your email address"); 8 formBuilder.addField("button","Submit"); 9 10 //当需要使用这个最终表单时,调用生成器的getForm方法来返回一个包含着所有表单域的
在需要在代码中通过一系列的小步骤来创建一个大型对象,再由应用程序在特定的时刻运用所创建的对象时,使用生成器模式最为合适。
5.2.4 原型模式
原型(Prototype)模式通过使用原型继承克隆已存在的对象来创建出新的对象。在学习了第1章之后,这一点对你来说还是熟悉的,即原型继承是JavaScript贯穿创建过程的继承类型。实现方法有两种:一种是使用一个已经存在的对象的Prototype属性,如我们已经见识过的在JavaScript中创建“类”所使用的;第二种是使用ECMAScript5的Object.create()方法,这是推荐的方法,但需要更好的浏览器支持才能作为唯一方法使用。代码5-7展示的是原型模式的使用方法,它所使用的是这两种技术当中的前者,而代码清单5-8演示的是后者。
代码清单5-7 使用prototype关键字实现原型模式
1 var textField, 2 emailField; 3 //定义一个Field“类”,用于创建表单元素 4 function Field(type,displayText){ 5 this.type=type||""; 6 this.displayText=displayText||""; 7 } 8 9 //使用Prototype属性来实现原型模式。所定义的方法将会应用于所有继承于此类的对象10 Field.prototype={11 getElement:function(){12 var field=document.createElement("input");13 field.setAttribute("type",this.type);14 field.setAttribute("placeholder",this.displayText);15 return field;16 }17 };18 19 //创建2个对象实例,二者都从Prototype中获得了getElement方法20 textField=new Field("text","Enter the first line of your address");21 emailField=new Field("email","Enter your email address");22 23 //一旦页面加载完成,便把这些对象中所保存的元素添加至当前页面24 window.addEventListener("load",function(){25 var bodyElement=document.body;26 bodyElement.appendChild(textField.getElement());27 bodyElement.appendChild(emailField.getElement());28 },false);
代码清单5-8 使用ECMAScript实现原型模式
1 //定义一个基础对象,该对象有两个属性,type和displayText,还有一个getElement()方法,此方法将创建一个HTML元素,使用上述两个属性对此元素进行设置 2 var field = { 3 type: "", 4 displayText: "", 5 getElement: function() { 6 var field = document.createElement("input"); 7 field.setAttribute("type", this.type); 8 field.setAttribute("placeholder", this.displayText); 9 return field;10 }11 },12 //基于基础对象创建一个新的对象,使用ECMAScript5的Object.create()方法来克隆原始对象,并未type和displayText这两项属性赋值。这样做的目的是为了创建出一个对象,13 //当调用该对象的getElement()方法时就能创建出一个元素14 textField = Object.create(field, {15 //Object.create()的第二个参数可以使用第1章描述的格式来改写第一个参数中的值16 'type': {17 value: "text",18 enumerable: true19 },20 'displayText': {21 value: "Enter the first line of your address",22 enumerable: true23 }24 }),25 //基于基础对象创建另一个新对象,使用的是不用的原型值,以便在调用该对象的getElement()方法时就可以创建出一个元素26 emailField = Object.create(field, {27 'type': {28 value: "email",29 enumerable: true30 },31 'displayText': {32 value: "Enter your email address",33 enumerable: true34 }35 });36 //调用两个对象的getElement()方法,一旦页面加载完成便将所创建的DOM元素添加至当前页面37 window.addEventListener("load", function() {38 var bodyElement = document.body;39 bodyElement.appendChild(textField.getElement());40 bodyElement.appendChild(emailField.getElement());41 },false);
在需要即时地克隆一个已存在的对象来创建新的对象时,或者要基于“类”模板来创建对象时,使用原型模式最为适合。
5.2.5 单例模式
当应用于JavaScript时,单例(Singleton)模式定义了一个对象的创建过程,此对象只有一个单独的实例。因此,单例的最简单形式可以是一个简单的对象直接量,其中封装了特定的相关行为,如代码清单5-9所示。
代码清单5-9 单例模式
1 //把相关的属性和方法聚集在一个单独的对象直接量内,我们称之为单例 2 var element = { 3 //创建一个数组,用于存储各个页面元素的引用 4 allElements: [], 5 6 //通过元素的ID获取对该元素的引用并保存它 7 get: function(id) { 8 var elem = document.getElementById(id); 9 this.allElements.push(elem);10 return elem;11 },12 //根据给定的类型创建一个新元素,并保存它13 create: function(type) {14 var elem = document.createElement(type);15 this.allElements.push(elem);16 return elem;17 },18 //返回所有保存的元素19 getAllElements: function() {20 return this.allElements;21 }22 },23 //获取对ID为header的页面元素的引用并进行保存24 header = element.get("header"),25 26 //创建一个新的元素27 input = element.create("input"),28 29 //这里包含着id为“header”以及新创建的元素30 allElements = element.getAllElements();31 //检查所保存的元素个数32 alert(allElements.length); //2
然而,在某些情况下,你可能会希望将一些初始化代码作为一个单例创建过程的一部分来执行。对于这点,可使用自执行函数(如代码清单5-10所示),并使用return关键字来体现出你所希望的代码其余部分可访问的对象结构。在下一章介绍模块模式(module)时,我们将深入了解自执行函数的使用方法。
代码清单5-10 使用自执行函数的单例模式
1 //定义一个单例,当中包含着与cookie操作相关的方法。初始化代码是通过使用自执行函数闭包实现的,这使得 2 //在创建单例时所执行的代码不是公共性的,不会被应用程序的其余部分访问,应用程序的其余部分只能访问单例中所暴露的方法。 3 var cookie=(function(){ 4 5 //cookie保存在document.cookie字符串中,由分号(;)进行分割 6 var allCookies=document.cookie.split(";"), 7 cookies={}, 8 cookiesIndex=0, 9 cookiesLength=allCookies.length,10 cookie;11 //循环遍历所有cookie,把它们添加至cookies对象,使用cookie的名称作为属性名称12 for (; cookiesIndex < cookiesLength; cookiesIndex++) {13 cookie=allCookies[cookiesIndex].split("=");14 cookies[unescape(cookie[0])]=unescape(cookie[1]); 15 }16 17 //这里所返回的方法将可以为在本代码清单顶部所定义的全局cookie变量所使用18 return {19 //创建一个函数,以cookie的名称获取其值20 get:function(name){21 return cookies[name]||"";22 },23 //创建一个函数来添加一个新的session cookie,session cookie即临时的会话cookie,浏览器关闭便自动失效24 set:function(name,value){25 //添加该新cookie至此cookies对象,同时也添加至document.cookie字符串中26 cookie[name]=value;27 document.cookie=escape(name)+"="+escape(value);28 }29 };30 }());31 //使用通过cookie单例所暴露的set方法来设置一个cookie32 cookie.set("name","wing");33 34 //检查该cookie是否正确设置35 alert(cookie.get("name"));//wing
许多开发者都会像这样使用单例模式来一些相关的代码封装和聚集为了一个层级化的结构,称其为设置命名空间(namespacing)。设置命名空间是一种在其他编程语言中很流行的做法,例如Java。像这样把所有的内容保持一个单独的全局变量里,可以减少对应用程序中所使用的任何第三方代码发生冲突的风险。请看代码清单5-11,当中显示的是一个基础的命名空间结构,用于把一些相关的代码一并保持在一个命名的小节中,以减少开发人员发生混淆的情况,并使维护和开发工作得到简化,使代码更容易阅读和理解。
代码清单5-11 使用单例模式实现命名空间的设置
1 //使用对象直接量来创建一个层级化分组的各项属性和方法的结构、称作“命名空间” 2 var myProject={ 3 data:{ 4 //每个嵌套的属性表示一个新的、更深层的命名空间层级 5 ajax:{ 6 //创建一个方法来发送Ajax GET请求 7 get:function(url,callback){ 8 var xhr =new XMLHttpRequest(), 9 STATE_LOADED=4,10 STAUS_OK=200;11 xhr.onreadystatechange=function(){12 if(xhr.readyState!==STATE_LOADED){13 return;14 }15 if(xhr.status===STAUS_OK){16 callback(xhr.responseText);17 }18 };19 xhr.open("GET",url);20 xhr.send();21 }22 }23 }24 };25 26 //命名空间建立后,使用点号标记法可以增加命名空间27 myProject.data.cookie={28 29 //创建一个方法,用于通过cookie的名称读取cookie的值30 get:function(name){31 var output="",32 escapedName=escape(name),33 start=document.cookie.indexOf(escapedName+"="),34 end=document.cookie.indexOf(";",start);35 36 end=end===-1?(document.cookie.length-1):end;37 38 if(start>=0){39 output=document.cookie.substring(start+escapedName.length+1,end);40 }41 return unescape(output);42 },43 44 //创建一个方法,用于设置cookie的“名称/值”对45 set:function(name,value){46 document.cookie=escape(name)+"="+escape(value);47 }48 };49 50 //使用点号标记法,通过"命名空间"层级来直接执行方法51 myProject.data.ajax.get("/user/123",function(response){52 alert("HTTP GET response received,User data:"+response);53 });54 //请注意是如何使用层级来增加最后的方法调用的明确性的55 myProject.data.cookie.set("company","AKQA");56 myProject.data.cookie.set("name","wing");57 58 //读取之前设定的各cookie值59 alert(myProject.data.cookie.get("company"));60 alert(myProject.data.cookie.get("name"));
当需要创建某对象的一个单独的实例以供代码的其余部分使用,或以命名空间来组织代码,在一个单独的全局变量下定义出层级结构来将代码划分出若干命名小节时,使用单例模式最为合适。