javascript设计模式-生成器模式(Builder)
1 <!doctype html> 2 <html lang="en"> 3 <head> 4 <meta charset="UTF-8"> 5 <title>生成器模式</title> 6 </head> 7 <body> 8 9 <script> 10 /** 11 * 生成器模式 12 * 13 * 定义: 14 * 将一个复杂对象的构建与它的表示分离,使得同样的构建过程可以创建不同的表示。 15 * 16 * 本质: 17 * 分离整体构建算法和部件构造。 18 * 生成器模式的重心在于分离整体构建算法和部件构造,而分步骤构建对象不过是整体构建算法的一个简单实现,或者说是一个附带产物。 19 * 20 * 要实现同样的构建过程可以创建不同的表现,那么一个自然的思路就是先把构建过程独立出来,在生成器模式中把它称为指导者,由它来指导装配过程,但是不负责每步具体的实现。当然,光有指导者是不够的,必须要有能具体实现每步的对象,在生成器模式中称这些实现对象为生成器。 21 * 这样一来,指导者就是可以重用的构建过程,而生成器是可以被切换的具体实现。 22 * 23 * 功能: 24 * 主要功能是构建复杂的产品,而且是细化的,分步骤的构建产品,也就是生成器模式中在一步一步解决构造复杂对象的问题。更为重要的是,这个构建的过程是统一的,固定不变的,变化的部分放到生成器部分了,只要配置不同的生成器,那么同样的构建过程,就能构建出不同的产品来。 25 * 生成器模式的重心在于分离构建算法和具体的构造实现,从而使得构建算法可以重用。具体的构造实现可以很方便地扩展和切换,从而可以灵活的组合来构造出不同的产品对象。 26 * 27 * 构成: 28 * 1.一个部分是Builder接口,这里是定义了如何构建各个部件,也就是知道每个部件功能如何实现,以及如何装配这些部件到产品中去。 29 * 2.另外一个部分是Director,Director是知道如何组合来构建产品,也就是说Director负责整体的构建算法,而且通常是分步骤地来执行。 30 * 不管如何变化,Builder模式都存在这么两个部分,一个部分是部件构造和产品装配,另一个部分是整体构建的算法。 31 * 32 * 使用: 33 * 应用生成器模式的时候,可以让客户端创造Director,在Director里面封装整体在、构建算法,然后让Director去调用Builder,让Builder来封装具体部件的构建功能。 34 * 还有一种退化的情况,就是让客户端和Director融合起来,让客户端直接去操作Builder,就好像是指导者自己想要给自己构建产品一样。 35 * 36 * 何时使用: 37 * 1.如果创建对象的算法,应该独立于该对象的组成部分以及它们的装配方式时。 38 * 2.如果同时一个构建过程有着不同的表示时。 39 * 40 */ 41 42 // 示例代码 43 44 // 生成器对象,创建一个产品对象所需的各个部件的操作 45 var Builder = function(){ 46 // 生成器最终构建的产品对象 47 var resultProduct; 48 49 // 获取生成器最终构建的产品对象 50 this.getResult = function(){ 51 return resultProduct; 52 }; 53 54 this.buildPart = function(){ 55 // 构建某个部件的功能处理 56 }; 57 } 58 59 // 指导者,指导使用生成器的接口来构建产品的对象 60 // 传入生成器对象 61 var Director = function(builder){ 62 this.builder = builder; 63 }; 64 Director.prototype = { 65 // 通过使用生成器接口来构建最终的产品对象 66 construct: function(){ 67 this.builder.buildPart(); 68 } 69 }; 70 71 (function(){ 72 73 // 生成器接口,定义创建一个输出文件对象所需的各个部件的操作 74 var Builder = function(){}; 75 Builder.prototype = { 76 // 构建输出文件的Header部分 77 buildHeader: function(ehm){}, 78 // 构建输出文件的Body部分 79 buildBody: function(mapData){}, 80 // 构建输出文件的Footer部分 81 buildFooter: function(efm){} 82 }; 83 84 // 实现到处数据到文本文件的生成器对象 85 var TxtBuilder = function(){ 86 this.stringBuffer = ''; 87 }; 88 TxtBuilder.prototype = { 89 __proto__: Builder.prototype, 90 91 buildBody: function(mapData){ 92 var tblName, edm; 93 for(tblName in mapData) { 94 this.stringBuffer += tblName + "\n"; 95 96 for(edm in mapData[tblName]) { 97 this.stringBuffer += edm.getProductId() + ',' + 98 edm.getPrice() + ',' + edm.getAmount() + '\n'; 99 } 100 } 101 }, 102 buildFooter: function(efm){ 103 this.stringBuffer += efm.getExportUser(); 104 }, 105 buildHeader: function(ehm){ 106 this.stringBuffer += ehm.getDepId() + ',' + 107 ehm.getExportDate() | '\n'; 108 }, 109 getResult: function(){ 110 return new Buffer(this.stringBuffer); 111 } 112 }; 113 114 // 实现导出数据到XML文件的生成器对象 115 var XmlBuilder = function(){ 116 this.stringBuffer = ''; 117 }; 118 XmlBuilder.prototype = { 119 __proto__: Builder, 120 buildBody: function(){}, 121 buildFooter: function(){}, 122 buildHeader: function(){}, 123 getResult: function(){} 124 }; 125 126 // 指导者,指导使用生成器的接口来构建输出的文件的对象 127 var Director = function(builder){ 128 // 传入生成器对象 129 this.builder = builder; 130 }; 131 Director.prototype = { 132 // 指导生成器构建最终的输出的文件的对象 133 construct: function(ehm, mapData, efm){ 134 this.builder.buildHeader(ehm); 135 this.builder.buildBody(mapData); 136 this.builder.buildFooter(efm); 137 } 138 }; 139 140 var textBuilder = new TxtBuilder(); 141 var director = new Director(textBuilder); 142 director.construct(ehm, mapData, efm); 143 144 var xmlBuilder = new XmlBuilder(); 145 var director2 = new Director(director); 146 director2.construct(ehm, mapData, efm); 147 148 }()); 149 150 /* 151 生成器模式的实现 152 153 1。生成器的实现 154 实际上在Builder接口的实现中,每个部件构建的方法里面,除了部件装配外,也可以实现如何具体地创建各个部件对象。也就是说每个方法都可以由两部分功能,一部分是创建部件对象,另一部分是组装部件。 155 在构建部件的方法里面可以实现选择并创建具体的部件对象,然后再把这个部件对象组装到产品对象中去。这样一来,Builder就可以和工厂方法配合使用了。 156 再进一步,如果在实现Builder的时候,只有创建对象的功能,而没有组装的功能。那么这个时候的Builder实现跟抽象工厂的实现是类似的。 157 这种情况下,Builder接口就类似抽象工厂的接口,Builder的具体实现就类似于具体的工厂,而且Builder接口里面定义的创建各个部件的方法也是有关联的,这些方法是构建一个复杂对象所需要的部件对象。 158 159 2.指导者的实现 160 在生成器模式里面,指导者承担的是整体构建算法部分,是相对不变的部分。因此在实现指导者的时候,把变化的部分分离出去很重要。 161 其实指导者分离出去的变化部分,就到了生成器那里,指导者知道整体的构建算法,却不知道如何具体的创建和装配部件对象。 162 因此真正的指导者实现,并不仅仅是简单地按照一定的顺序调用生成器的方法来生成对象。应该是有较为复杂的算法和运算过程,在运算过程中根据需要,才会调用生成器的方法来生成部件对象。 163 164 3.指导者和生成器的交互 165 在生成器模式里面,指导者和生成器的交互是通过生成器的buildPart方法来完成的。 166 指导者通常会实现比较复杂的算法或者是运算过程,在实际开发中很可能会有以下的情况: 167 1) 在运行指导者的时候,会按照整体构建算法的步骤进行运算,可能先运行前几步运算,到了某一步骤,需要具体创建某个部件对象了,然后就调用Builder中创建相应部件的方法来创建具体的部件。同时,把前面运算得到的数据传递给Builder,因为在Builder内部实现创建和组装部件的时候,可能会需要这些数据。 168 2) Builder创建完具体的部件对象后,会把创建好的部件对象返回给指导者,指导者继续后续的算法运算,可能会用到已经创建好的对象。 169 3)如此反复下去,知道整个构建算法运行完成,那么最终的产品对象也就创建好了。 170 171 可以看出指导者和生成器是需要交互的,方式就是用过生成器方法的参数和返回值,来回地传递数据。事实上,指导者使用过委托的方式把功能交给生成器去完成。 172 173 4.返回装配好的产品的方法 174 标准的生成器模式中,在Builder实现里面会提供一个返回装配好的产品的方法,在Builder接口上是没有的。它考虑的是最终的对象一定要通过部件构建和装配,才算真正创建了,而具体干活的就是Builder实现,虽然指导者也参与了,但是指导者是不负责具体的部件创建和组装的,因此客户端是从Builder实现里面获取最终装配好的产品。 175 176 5.关于被构建的产品的接口 177 在使用生成器模式的时候,大多数情况下是不知道最终构建出来的产品是什么,所以一般不需要对产品定义抽象接口,因为最终构建的产品千差万别,给这些产品定义公共接口几乎是没有意义的。 178 */ 179 180 // 使用生成器模式构建复杂对象 181 182 var InsuranceContract = (function(){ 183 // 保险合同对象 184 var InsuranceContract = function(builder){ 185 this.contractId = builder.getContractId(); 186 this.personName = builder.getPersonName(); 187 this.companyName = builder.getCompanyName(); 188 this.beginDate = builder.getBeginDate(); 189 this.endDate = builder.getEndDate(); 190 this.otherDate = builder.getOtherDate(); 191 }; 192 InsuranceContract.prototype = { 193 someOperation: function(){ 194 console.log('Now in Insurance Contract someOperation = ' + this.contractId); 195 } 196 }; 197 198 // 构造保险合同对象的构造器 199 var ConcreteBuilder = function(contractId, beginDate, endDate){ 200 this.contractId = contractId; 201 this.beginDate = beginDate; 202 this.endDate = endDate; 203 }; 204 ConcreteBuilder.prototype = { 205 setPersonName: function(personName){ 206 this.personName = personName; 207 return this; 208 }, 209 setCompanyName: function(companyName){ 210 this.companyName = companyName; 211 return this; 212 }, 213 setOtherDate: function(otherData){ 214 this.otherData = otherData; 215 return this; 216 }, 217 getContractId: function(){ 218 return this.contractId; 219 }, 220 getPersonName: function(){ 221 return this.personName; 222 }, 223 getCompanyName: function(){ 224 return this.companyName; 225 }, 226 getBiginDate: function(){ 227 return this.beginDate; 228 }, 229 getEndDate: function(){ 230 return this.endDate; 231 }, 232 getOtherData: function(){ 233 return this.otherData; 234 }, 235 236 // 构建真正的对象并返回 237 // 添加一些约束规则 238 build: function(){ 239 if(!this.contractId || this.contractId.trim().length === 0) { 240 throw new Error('合同编号不能为空'); 241 } 242 var signPerson = this.personName && this.personName.trim().length > 0; 243 var signCompany = this.companyName && this.companyName.trim().length > 0; 244 245 if(signPerson && signCompany) { 246 throw new Error('一份合同不能同时与人和公司签订'); 247 } 248 249 if(!signPerson && !signCompany) { 250 throw new Error('一份合同不能没有签订对象'); 251 } 252 253 if(this.beginDate <= 0) { 254 throw new Error('合同必须有保险开始生效的日期'); 255 } 256 257 if(this.endDate <= 0) { 258 throw new Error('合同必须有保险失效的日期'); 259 } 260 261 if(this.endDate <= this.beginDate) { 262 throw new Error('保险失效的日期必须大于生效日期'); 263 } 264 265 return new InsuranceContract(this); 266 } 267 }; 268 269 // 把构建对象和被构建对象合并 270 InsuranceContract.ConcreteBuilder = ConcreteBuilder; 271 272 return InsuranceContract; 273 }()); 274 275 276 var builder = new InsuranceContract.ConcreteBuilder('001', 123456, 6789); 277 var contract = builder.setPersonName('luke').setOtherData('test').build(); 278 contract.someOperation(); 279 280 281 /* 282 相关模式 283 284 1.生成器模式与工厂方法模式 285 286 这两个模式可以组合使用。 287 生成器模式的Builder实现中,通常需要选择具体的部件实现。一个可行的方案就是实现成为工厂方法,通过工厂方法来获取具体的部件对象,然后再进行部件的装配。 288 289 2.生成器模式与抽象工厂模式 290 291 这两个模式既相似又有区别,也可以组合使用。 292 区别:抽象工厂模式的主要目的是创建产品簇,这个产品簇里面的单个产品就相当于是构成一个复杂对象的部件对象,抽象工厂对象创建完成后就立即返回整个产品簇;而生成器模式的主要目的是按照构造算法,一步一步来构建一个复杂的产品对象,桐城要等到整个构建过程结束以后没才会得到最终的产品对象。 293 组合使用:在生成器模式的Builder实现中,需要创建各个部件对象,而这些部件对象是有关联的,通常是构成一个复杂对象的部件对象。也就是说,Builder实现中,需要获取构成一个复杂对象的产品簇,就可以使用抽象工厂模式来实现。有抽象工厂模式负责部件对象创建,Builder实现里面则主要负责产品对象整体的构建了。 294 295 3.生成器模式和模板方法模式 296 297 这也是两个非常相似的模式。 298 模板方法模式主要是用来定义算法的骨架,把算法中某些步骤延迟到子类中实现。生成器模式Director用来定义整体的构建算法,把算法中某些涉及到具体部件对象的创建和装配功能,委托给具体的Builder来实现。 299 从实质上看,都是定义一个固定的算法骨架,然后把算法中的某些具体步骤交给其他类来实现,都能实现整体算法步骤和某些具体步骤实现的分离。 300 区别: 301 1).目的:生成器模式用来构建复杂对象。模板模式用来定义算法骨架,尤其是一些复杂的业务功能的处理算法的骨架; 302 2)。实现:生成器模式采用委托的方法,模板模式采用继承的方法。 303 3)。复杂度:生成器模式需要组合Director和Builder对象,然后才能开始构建,要等构建后才能获得最终的对象,而模板方法就没这么麻烦,直接使用子类对象即可。 304 305 4.生成器模式和组合模式 306 307 可以组合使用。 308 对于复杂的组合结构,可以使用生成器模式来一步一步构建。 309 */ 310 311 312 // http://www.dofactory.com/javascript-builder-pattern.aspx 313 314 function Shop() { 315 this.construct = function(builder) { 316 builder.step1(); 317 builder.step2(); 318 return builder.get(); 319 } 320 } 321 322 function CarBuilder() { 323 this.car = null; 324 this.step1 = function() { 325 this.car = new Car(); 326 }; 327 this.step2 = function() { 328 this.car.addParts(); 329 }; 330 this.get = function() { 331 return this.car; 332 }; 333 } 334 335 function TruckBuilder() { 336 this.truck = null; 337 this.step1 = function() { 338 this.truck = new Truck(); 339 }; 340 this.step2 = function() { 341 this.truck.addParts(); 342 }; 343 this.get = function() { 344 return this.truck; 345 }; 346 } 347 348 function Car() { 349 this.doors = 0; 350 this.addParts = function() { 351 this.doors = 4; 352 }; 353 this.say = function() { 354 log.add("I am a " + this.doors + "-door car"); 355 }; 356 } 357 358 function Truck() { 359 this.doors = 0; 360 this.addParts = function() { 361 this.doors = 2; 362 }; 363 this.say = function() { 364 log.add("I am a " + this.doors + "-door truck"); 365 }; 366 } 367 368 // log helper 369 var log = (function () { 370 var log = ""; 371 return { 372 add: function (msg) { log += msg + "\n"; }, 373 show: function () { alert(log); log = ""; } 374 } 375 })(); 376 377 function run() { 378 var shop = new Shop(); 379 380 var carBuilder = new CarBuilder(); 381 var truckBuilder = new TruckBuilder(); 382 383 var car = shop.construct(carBuilder); 384 var truck = shop.construct(truckBuilder); 385 386 car.say(); 387 truck.say(); 388 389 log.show(); 390 } 391 </script> 392 </body> 393 </html>