本章主要学习结构性设计模式,前一章介绍的创建型设计模式侧重于对象的处理,而结构型设计模式则有助于把多个对象整合为一个更大型的、更有组织的代码库。它们具有灵活性,可维护性,可扩展性,并能够确保当系统中的某一部分发生变更时,不必完全重写其余部分进行适应。结构型模式还可用于帮助我们与其他代码结构(在我们的应用程序中需要简易地实现与这些代码结构的协同运作)
6.1 适配器模式
适配器(adapter)模式是一种很有用的设计模式。当需要关联两个或更多代码组件时便可应用此模式,否则这些代码将无法正常关联在一起。相类似地,当一个你之前开的API出现更新而不能再按相同的方法进行调用时,此模式也能帮上大忙。适配器提供了旧版本与新版本API之间的对接,能够帮助那些使用你的API的用户迁移至新API,使得用户在分享你改进后的代码的同时而又不必更改其原来的旧代码。代码清单6-1演示了如果使用此模式来为代码创建一个适配器,以将新的API映射至旧API。
1 //假设以下节课深藏在你庞大的代码库中,用于通过HTTP发出Ajax请求 2 var http = { 3 makeRequest: function(type, url, callback, data) { 4 var xhr = new XMLHttpRequest(), 5 STATE_LOADED = 4, 6 STATUS_OK = 200; 7 8 xhr.onreadystatechange = function() { 9 if(xhr.readyState !== STATE_LOADED) { 10 return; 11 } 12 13 if(xhr.status === STATUS_OK) { 14 callback(xhr.responseText); 15 } 16 }; 17 xhr.open(type.toUpperCase(), url); 18 xhr.send(data); 19 } 20 }; 21 22 //以上定义的http.makeRequest()方法可按如下方式进行调用,以对系统中的ID为“12345”的用户的数据进行获取和更新 23 http.makeRequest("get", "/user/12345", function(response) { 24 alert("HTTP GET response received,User data:" + response); 25 }); 26 27 http.makeRequest("post", "/user/12345", function(response) { 28 alert("HTTP POST response received,New User data:" + response); 29 }, "company=AKQA&name=wing"); 30 31 //现在,假设你要对项目进行重构,你决定引入一个新的结构,使用命名空间,并把makeRequest()方法划分为 32 //两个独立的方法来发出HTTP GET和POST请求 33 var myProject = { 34 data: { 35 ajax: (function() { 36 function createRequestObj(callback) { 37 var xhr = new XMLHttpRequest(), 38 STATE_LOADED = 4, 39 STATUS_OK = 200; 40 xhr.onreadystatechange = function() { 41 if(xhr.readyState !== STATE_LOADED) { 42 return; 43 } 44 45 if(xhr.status === STATUS_OK) { 46 callback(xhr.responseText); 47 } 48 }; 49 return xhr; 50 } 51 return { 52 get: function(url, callback) { 53 var requestObj = createRequestObj(callback); 54 requestObj.open("GET", url); 55 requestObj.send(); 56 57 }, 58 post: function(url, data, callback) { 59 var requestObj = createRequestObj(callback); 60 requestObj.open("POST", url); 61 requestObj.send(data); 62 } 63 64 }; 65 }()) 66 } 67 }; 68 69 //新的get()和post()方法可按如下方式调用 70 myProject.data.ajax.get("/user/12345",function(response){ 71 alert("Refactored HTTP GET response received.User data:"+response); 72 }); 73 74 myProject.data.ajax.post("/user/12345","company=AKQA&name=wing",function(response){ 75 alert("Refactored HTTP GET response received.New User data:"+response); 76 }); 77 78 //为了避免在代码库中的其余部分重写每一个对http.makeRequest()方法的调用,你可以创建一个适配器来映射 79 //旧接口至新方法。配置器需要使用与所要替换掉的原方法相同的输入参数,并在适配器内部调用新方法 80 function httpToAjaxAdapter(type,url,callback,data){ 81 if(type.toLowerCase()==="get"){ 82 myProject.data.ajax.get(url,callback); 83 }else if(type.toLowerCase()==="post"){ 84 myProject.data.ajax.post(url,data,callback); 85 } 86 } 87 88 //最后,应用配置器来代替员原来的方法 89 //这样,它将会映射旧接口至新方法,而不需要同时重写整个代码的其余部分 90 http.makeRequest=httpToAjaxAdapter; 91 92 //按照原方法的使用方式使用该新的适配器————在内部,它将调用新的代码,但在外部, 93 //它看起来有与旧的makeRequest()方法一模一样 94 http.makeRequest("get","/user/123456",function(response){ 95 alert("Refactored HTTP GET response received.User data:"+response); 96 }); 97 98 http.makeRequest("post","/user/12345",function(response){ 99 alert("Refactored HTTP GET response received.New User data:"+response); 100 },"company=AKQA&name=wing");
当需要把不同的代码进行关联,否则这些代码无法兼容在一起工作时,使用适配器模式最为合适。例如,当某个外部API进行了更新时,可以创建一个是适配器来映射各新方法至旧方法,以避免更改依赖这些方法的其余代码。
6.2 组合模式
组合(composite)模式为一个或多个对象创建了一个接口,使终端用户不需要知道他们所处里对象的个数。当你希望能够简化其他开发者对你的函数的访问方法时,该模式很有帮助。无论他人向同一方法传入的是一个单独对象还是一个由对象组成的数组,都不需要区别对待。代码清单6-2展示了组合模式的一个简单例子。用户可以添加class标签特性名称至一个或多个DOM节点,而不需要知道是否应将一个或多个DOM节点传给该方法。
代码清单6-2 组合模式
1 var elements = { 2 //定义一个方法按tag名称获取DOM元素。如果只发现一个元素,则它作为一个单独的节点返回, 3 //如果发现多个元素,则返回这些元素所组成的数组 4 get: function(tag) { 5 var elems = document.getElementsByTagName(tag), 6 elemsIndex = 0, 7 elemsLength = elems.length, 8 output = []; 9 10 //把所找到的元素结构转化为一个标准数组 11 for(; elemsIndex < elemsLength; elemsIndex++) { 12 output.push(elems[elemsIndex]); 13 } 14 //如果只找到一个元素,则返回该独立元素,否则返回所找到的各个元素所组成的数组 15 return output.length === 1 ? output[0] : output; 16 }, 17 18 //定义一个组合方法,用于为一个或多个元素添加class标签特性class名称,无论在执行时有多少个元素被传入都可实现 19 addClass: function(elems, newClassName) { 20 var elemIndex = 0, 21 elemLength = elems.length, 22 elem; 23 24 //判断所传入的元素究竟是数组还是一个单独对象 25 if(Object.prototype.toString.call(elems) === "[object Array]") { 26 //如果是数组,循环遍历每一个元素并为每个元素都增加class标签特性class名称 27 for(; elemIndex < elemLength; elemIndex++) { 28 elem = elems[elemIndex]; 29 elem.className += (elem.className === "" ? "" : " ") + newClassName; 30 } 31 } else { 32 //如果传入的是单独元素,则为其增加class标签特性class名称值 33 elems.className += (elems.className === "" ? "" : " ") + newClassName; 34 } 35 } 36 }; 37 38 //使用该elements.get()方法来找出当前页面的单独的<body>元素,已经<a>元素(可能有很多个) 39 var body = elements.get("body"), 40 links = elements.get("a"); 41 42 //该组合方式elements.addClass()为单独元素和多个元素给出了相同的使用接口,很明显地简化了该方法的使用 43 elements.addClass(body, "has-js"); 44 elements.addClass(links, "custom-link");
若不希望那些正与你的方法进行交互的开发者操心需要传入多少个对象作为方法参数,使用组合模式最为合适,这样可以简化方法的调用。
6.3 装饰模式
装饰(decorator)模式用于为某个“类”创建的对象扩展和定制额外的方法和属性,避免了因创建大量的子类而变得难以维护。其实现方法时,通过有效地将对象包装在另一个对象中,此另一个对象实现了相同的公共方法,根据我们所要增加的行为对相关方法进行重写。
代码清单6-3演示了一个例子,当中创建了若干装饰者,每个装饰者都会对一个已存在的对象增加额外的属性和行为。