javascript设计模式-模板方法模式(Template)
1 <!DOCTYPE HTML> 2 <html lang="en-US"> 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 * 作为父类的模板会在需要的时候,调用子类相应的方法,也就是由父类来找子类,而不是让子类来找父类。 29 * 30 * 对设计原则的体现 31 * 模板方法很好地体现了开闭原则和里氏原则。 32 * 首先从设计上分离变与不变,然后把不变的部分抽取出来,定义到父类中,比如算法骨架,一些公共的,固定的实现等。这些不变的部分被封闭起来,尽量不去修改它们。想要扩展新的功能,那就是用子类来扩展,通过子类来实现可变化的步骤,对于这种新增功能的做法是开放的。 33 * 其次,能够实现统一的算法骨架,通过切换不同的具体实现来切换不同的功能,一个根本原因就是里氏替换原则,遵循这个原则,保证所有的子类实现的是同一个算法模板,并能在使用模板的地方,根据需要切换不同的具体实现。 34 * 35 * 相关模式 36 * 37 * 模板方法模式和工厂方法模式 38 * 可以配合使用 39 * 模板方法模式可以通过工厂方法来获取需要调用的对象。 40 * 41 * 模板方法模式和策略模式 42 * 两者有些相似,但是有区别 43 * 从表面看,两个模式都能实现算法的封装,但是模板方法封装的是算法的骨架,这个算法骨架是不变的,变化的是算法中某些步骤的具体实现;而策略模式是把某个步骤的具体实现算法封装起来,所有封装的算法对象是等价的,可以相互替换。 44 * 因此,可以在模板方法中使用策略模式,就是把那些变化的算法步骤通过使用策略模式来实现,但是具体选取哪个策略还是要由外部来确定,而整体的算法步骤,也就是算法骨架则由模板方法来定义了。 45 */ 46 47 (function () { 48 // 示例代码 49 50 // 定义模板方法,原语操作等的抽象类 51 function AbstractClass() { 52 } 53 54 AbstractClass.prototype = { 55 // 原语操作1,所谓的原语操作就是抽象的操作,必须要由子类提供实现 56 doPrimitiveOperation1: function () { 57 }, 58 // 原语操作2 59 doPrimitiveOperation2: function () { 60 }, 61 // 模板方法,定义算法骨架 62 templateMethod: function () { 63 this.doPrimitiveOperation1(); 64 this.doPrimitiveOperation2(); 65 } 66 }; 67 68 function ConcreteClass() { 69 } 70 71 ConcreteClass.prototype = { 72 __proto__: AbstractClass.prototype, 73 74 doPrimitiveOperation1: function () { 75 // 具体的实现 76 }, 77 doPrimitiveOperation2: function () { 78 // 具体的实现 79 } 80 }; 81 }()); 82 83 (function(){ 84 // 验证人员登录的例子 85 86 // 封装进行登录控制所需要的数据 87 function LoginModel(){ 88 // 登录人员编号 89 this.loginId; 90 // 登录密码 91 this.pwd; 92 } 93 94 // 登录控制的模板 95 function LoginTemplate(){} 96 LoginTemplate.prototype = { 97 // 判断登录数据是否正确,也就是是否能登录成功 98 login: function(loginModel){ 99 var dbLm = this.findLoginUser(loginModel.loginId); 100 101 if(dbLm) { 102 // 对密码进行加密 103 var encryptPwd = this.encryptPwd(loginModel.pwd); 104 // 把加密后的密码设置回到登录数据模型中 105 loginModel.pwd = encryptPwd; 106 // 判断是否匹配 107 return this.match(loginModel, dbLm); 108 } 109 110 return false; 111 }, 112 // 根据登录编号来查找和获取存储中相应的数据 113 findLoginUser: function(loginId){}, 114 // 对密码数据进行加密 115 encryptPwd: function(pwd){ 116 return pwd; 117 }, 118 // 判断用户填写的登录数据和存储中对应的数据是否匹配得上 119 match: function(lm, dbLm){ 120 return lm.loginId === dbLm.loginId 121 && lm.pwd === dbLm.pwd; 122 } 123 }; 124 125 // 普通用户登录控制的逻辑处理 126 function NormalLogin(){} 127 NormalLogin.prototype = { 128 __proto__: LoginTemplate.prototype, 129 130 findLoginUser: function(loginId){ 131 var lm = new LoginModel(); 132 lm.loginId = loginId; 133 lm.pwd = 'testpwd'; 134 return lm; 135 } 136 }; 137 138 // 工作人员登录控制的逻辑处理 139 function WorkerLogin(){} 140 WorkerLogin.prototype = { 141 __proto__: LoginTemplate.prototype, 142 143 findLoginUser: function(loginId){ 144 var lm = new LoginModel(); 145 lm.loginId = loginId; 146 lm.pwd = 'workerpwd'; 147 return lm; 148 }, 149 encryptPwd: function(pwd){ 150 console.log('使用MD5进行密码加密'); 151 return pwd; 152 } 153 }; 154 155 var lm = new LoginModel(); 156 lm.loginId = 'admin'; 157 lm.pwd = 'workerpwd'; 158 159 var lt = new WorkerLogin(); 160 var lt2 = new NormalLogin(); 161 162 var flag = lt.login(lm); 163 console.log('可以登录工作平台=' + flag); 164 165 var flag2 = lt2.login(lm); 166 console.log('可以进行普通人员登录=' + flag2); 167 168 169 // another style 170 171 function test(){ 172 var crypto = require('crypto'); 173 function createHmac(){ 174 return crypto.createHmac('sha1', 'password'); 175 } 176 177 // 封装进行登录控制所需要的数据 178 function LoginModel(){ 179 // 登录人员编号 180 this.loginId; 181 // 登录密码 182 this.pwd; 183 } 184 185 // 登录控制的模板 186 function LoginTemplate(){} 187 LoginTemplate.prototype = { 188 // 判断登录数据是否正确,也就是是否能登录成功 189 login: function(loginModel){ 190 var dbLm = this.findLoginUser(loginModel.loginId); 191 192 if(dbLm) { 193 // 对密码进行加密 194 var encryptPwd = this.encryptPwd(loginModel.pwd); 195 // 把加密后的密码设置回到登录数据模型中 196 loginModel.pwd = encryptPwd; 197 // 判断是否匹配 198 return this.match(loginModel, dbLm); 199 } 200 201 return false; 202 }, 203 // 根据登录编号来查找和获取存储中相应的数据 204 findLoginUser: function(loginId){}, 205 // 对密码数据进行加密 206 encryptPwd: function(pwd){ 207 return pwd; 208 }, 209 // 判断用户填写的登录数据和存储中对应的数据是否匹配得上 210 match: function(lm, dbLm){ 211 return lm.loginId === dbLm.loginId 212 && lm.pwd === dbLm.pwd; 213 } 214 }; 215 216 function createLoginClass(prop){ 217 Template.prototype = LoginTemplate.prototype; 218 219 return Template; 220 221 function Template(){ 222 for(var i in prop) { 223 if(!prop.hasOwnProperty(i)) continue; 224 225 this[i] = prop[i]; 226 } 227 } 228 } 229 230 var NormalLogin = createLoginClass({ 231 findLoginUser: function(loginId){ 232 var lm = new LoginModel(); 233 lm.loginId = loginId; 234 lm.pwd = 'testpwd'; 235 return lm; 236 } 237 }); 238 239 var WorkerLogin = createLoginClass({ 240 findLoginUser: function(loginId){ 241 var lm = new LoginModel(); 242 lm.loginId = loginId; 243 lm.pwd = createHmac().update('workerpwd').digest("hex"); 244 return lm; 245 }, 246 encryptPwd: function(pwd){ 247 console.log('使用MD5进行密码加密'); 248 return createHmac().update(pwd).digest('hex'); 249 } 250 }); 251 252 var lm = new LoginModel(); 253 lm.loginId = 'admin'; 254 lm.pwd = 'workerpwd'; 255 256 var lt = new WorkerLogin(); 257 var lt2 = new NormalLogin(); 258 259 var flag = lt.login(lm); 260 console.log('可以登录工作平台=' + flag); 261 262 var flag2 = lt2.login(lm); 263 console.log('可以进行普通人员登录=' + flag2); 264 265 266 267 // 扩展登录控制 268 269 function NormalLoginModel(){ 270 LoginModel.call(this); 271 272 // 密码验证问题 273 this.question; 274 // 密码验证答案 275 this.answer; 276 } 277 278 function NormalLogin2(){} 279 NormalLogin2.prototype = { 280 __proto__: LoginTemplate, 281 282 findLoginUser: function(loginId){ 283 var nlm = new NormalLoginModel(); 284 nlm.loginId = loginId; 285 nlm.pwd = 'testpwd'; 286 nlm.question = 'testQuestion'; 287 nlm.answer = 'testAnswer'; 288 289 return nlm; 290 }, 291 match: function(lm, dblm){ 292 var f1 = LoginTemplate.prototype.match.apply(this,arguments); 293 294 if(f1) { 295 return dblm.question === lm.question 296 && dblm.answer === lm.answer; 297 } 298 299 return false; 300 } 301 }; 302 303 var nlm = new NormalLoginModel(); 304 nlm.loginId = 'testUser'; 305 nlm.pwd = 'testpwd'; 306 nlm.question = 'testQuestion'; 307 nlm.answer = 'testAnswer'; 308 var lt3 = new NormalLogin2(); 309 var flag3 = lt3.login(nlm); 310 console.log('可以进行普通人员加强版登录=' + flag3); 311 312 } 313 314 }()); 315 316 317 (function () { 318 // 咖啡因饮料是一个抽象类 319 var CaffeineBeverage = function () { 320 }; 321 CaffeineBeverage.prototype = { 322 /*---模板方法 ----*/ 323 /** 324 * 它的用作一个算法的模板,在这个例子中,算法是用来制作咖啡因饮料的, 325 * 在这个模板中,算法内的每一个步骤都被一个方法代表了 326 */ 327 prepareRecipe: function () { 328 this.boilWater(); 329 this.brew(); 330 this.pourInCup(); 331 this.addConditions(); 332 }, 333 /*----------------*/ 334 /* 因为咖啡和茶处理这些方法的做法不同,所以这两个方法必须被声明为抽象 */ 335 brew: function () { 336 throw new Error('abstract brew method should be written.'); 337 }, 338 addConditions: function () { 339 throw new Error('abstract addConditions method should be written.'); 340 }, 341 /* ------------------------------- */ 342 boilWater: function () { 343 console.log('boiling water'); 344 }, 345 pourInCup: function () { 346 console.log('pouring into cup'); 347 } 348 }; 349 350 var Tea = function () { 351 }; 352 Tea.prototype = { 353 __proto__: CaffeineBeverage.prototype, 354 355 brew: function () { 356 console.log('steeping the tea.'); 357 }, 358 addConditions: function () { 359 console.log('adding lemon'); 360 } 361 }; 362 363 var Coffee = function () { 364 }; 365 Coffee.prototype = { 366 __proto__: CaffeineBeverage.prototype, 367 368 brew: function () { 369 console.log('Dripping Coffee through filter'); 370 }, 371 addConditions: function () { 372 console.log('adding Sugar and Milk'); 373 } 374 }; 375 376 var myTea = new Tea(); 377 myTea.prepareRecipe(); 378 }()); 379 380 /* 381 由CaffeineBeverage类主导一切,它拥有算法,而且保护这个算法。对子类来说,CaffeineBeverage类deep存在,可以将代码的复用最大化。算法只存在于一个地方,所以容易修改。这个模板方法提供了一个框架,可以让其他的咖啡因饮料插进去,新的咖啡因饮料只需要实现自己的方法就可以了。CaffeeineBeverage类专注在算法本身,而由子类提供完整的实现。 382 */ 383 384 (function(){ 385 /* 386 对模板方法进行挂钩 387 388 钩子是一种被声明在抽象类中的方法,但只有空的或者默认的实现。钩子的存在,可以让子类有能力对算法的不同点进行挂钩。要不要挂钩,由子类自行决定。 389 */ 390 391 // 高层组件,只有在需要子类实现某个方法时,方调用子类。 392 var CaffeineBeverageWithHook = function () { 393 }; 394 CaffeineBeverageWithHook.prototype = { 395 prepareRecipe: function () { 396 this.boilWater(); 397 this.brew(); 398 this.pourInCup(); 399 /*---------- 钩子 ----------*/ 400 if (this.customerWantsCondiments()) { 401 this.addCondiments(); 402 } 403 /*---------------------------*/ 404 }, 405 brew: function () { 406 throw new Error('brew method should be rewritten.'); 407 }, 408 addCondiments: function () { 409 throw new Error('addCondiments method should be written.'); 410 }, 411 boilWater: function () { 412 console.log('Boiling water'); 413 }, 414 pourInCup: function () { 415 console.log('pourng into cup'); 416 }, 417 /*------- 钩子方法 ------*/ 418 customerWantsCondiments: function () { 419 return true; 420 } 421 /*----------------------*/ 422 }; 423 424 var CoffeeWithHook = function () { 425 }; 426 CoffeeWithHook.prototype = { 427 __proto__: CaffeineBeverageWithHook.prototype, 428 429 brew: function () { 430 console.log('Dripping coffee through filter'); 431 }, 432 customerWantsCondiments: function () { 433 var answer = this.getUSerInput(); 434 435 return answer === true; 436 }, 437 getUSerInput: function () { 438 return confirm('Would you like milk and sugar with your coffee (y/n)?'); 439 }, 440 addCondiments: function () { 441 console.log('adding sugar and milk'); 442 } 443 }; 444 445 var coffeeHook = new CoffeeWithHook(); 446 coffeeHook.prepareRecipe(); 447 }()); 448 449 /* 450 好莱坞原则 451 452 别调用我们,我们会调用你。 453 454 好莱坞原则可以给我们一种防止“依赖腐败”的方法。当高层组件依赖低层组件,而低层组件又依赖高层组件,而高层组件又依赖边侧组件,而边侧组件又依赖低层组件时,依赖腐败就发生了。在这种情况下,没有人可以轻易地搞懂系统是如何设计的。 455 在好莱坞原则之下,我们允许低层组件将自己挂钩到系统上,但是高层组件会决定什么时候和怎样使用这些低层组件。换句话说,高层组建对待低层组件的方式是“别调用我们,我们会调用你”。 456 */ 457 458 (function () { 459 /* 460 抽象类不一定包含抽象方法;有抽象方法的类一定是抽象类。 461 “既要约束子类的行为,又要为子类提供公共功能”的时候使用抽象类。 462 */ 463 464 var Duck = function (name, weight) { 465 this.name = name; 466 this.weight = weight; 467 }; 468 Duck.prototype = { 469 toString: function () { 470 return name + ' weighs ' + this.weight; 471 } 472 }; 473 474 var ducks = [ 475 new Duck('A', 8), 476 new Duck('B', 2), 477 new Duck('C', 7), 478 new Duck('D', 2), 479 new Duck('E', 10), 480 new Duck('E', 2) 481 ]; 482 console.log('before'); 483 display(ducks); 484 485 /*---------- 内置对象的模板方法 --------*/ 486 ducks.sort(function (obj1, obj2) { 487 return obj1.weight - obj2.weight; 488 }); 489 /*-------------------------------------*/ 490 491 console.log('after'); 492 display(ducks); 493 494 function display(arr) { 495 for (var i = 0, len = arr.length; i < len; i++) { 496 console.log(arr[i] + ''); 497 } 498 } 499 500 /* 501 排序的算法步骤是固定的,也就是算法骨架是固定的了,只是其中具体比较数据大小的步骤,需要由外部来提供。 502 排序的实现,实际上组合使用了模板方法模式和策略模式,从整体来看是模板方法模式,但到了局部,比如排序比较算法的实现上,就是用的是策略模式了。 503 */ 504 }()); 505 506 /** 507 * 模板方法里面包含的操作类型: 508 * 1.模板方法: 就是定义算法骨架的方法。 509 * 2.具体的操作: 在模板中直接实现某些步骤的方法。通常这些步骤的实现算法是固定的,而且是不怎么变化的,因此可以将其当作公共功能实现在模板中。如果不需为子类提供访问这些方法的话,还可以是私有的。这样子类的视线就相对简单些。 510 * 3.具体的AbstractClass操作: 在模板中实现某些公共的功能,可以提供给子类使用,一般不是具体的算法步骤实现,而是一些辅助的公共功能。 511 * 4.原语操作: 就是在模板中定义的抽象操作,通常是模板方法需要调用的操作,时必须的操作,而且在父类中还没有办法确定下来如何实现,需要子类来真正实现的方法。 512 * 5.钩子操作: 在模板中定义,并提供默认实现的操作。这些方法通常被视为可扩展的点,但不是必需的,子类可以有选择地覆盖这些方法,已提供新的实现来扩展功能。 513 * 6.Factory Method:在模板方法中,如果需要得到某些对象实例的话,可以考虑通过工厂方法模式来获取,把具体的构建对象的实现延迟到子类中去。 514 */ 515 516 (function(){ 517 // 一个较为完整的模板定义示例 518 519 function AbstractTemplate(){ 520 // 模板方法,定义算法骨架 521 this.templateMethod = function(){ 522 operation1(); 523 this.operation2(); 524 this.doPrimitiveOperation1(); 525 this.dePrimitiveOperation2(); 526 this.hookOperation(); 527 } 528 // 具体操作2,算法中的步骤,固定实现,子类可能需要访问 529 this.operation2 = function(){}; 530 // 具体的AbstractClass操作,子类的公共方法,但通常不是具体的算法 531 this.commondOperationi = function(){}; 532 // 原语操作1,算法中的步骤,父类无法确定如何真正实现,需要子类来实现 533 this.doPrimitiveOperation1 = function(){}; 534 this.doPrimitiveOperation2 = function(){}; 535 // 钩子操作,算法中的步骤,可选,提供默认实现 536 // 由子类选择并具体实现 537 this.hookOperationi = function(){}; 538 539 // 具体操作1,算法中的步骤,固定实现,而且子类不需要访问 540 function operation1(){} 541 // 工厂方法,创建某个对象,在算法实现中可能需要 542 this.createOneObject = function(){}; 543 } 544 }()); 545 546 /* 547 优点 548 实现代码复用。 549 模板方法模式是一种实现代码复用的很好的手段。通过把子类的公共功能提炼和抽取,把公共部分放到模板中去实现。 550 551 552 缺点 553 算法骨架不容易升级 554 模板方法模式最基本的功能就是通过模板的制定,把算法骨架完全固定下来。事实上模板和子类是非常耦合的,如果要对模板中的算法骨架进行变更,可能就会要求所有相关的子类进行相应的变化。所以抽取算法骨架的时候要特别小心,尽量确保不会变化的部分才放到模板中。 555 */ 556 557 /* 558 何时使用 559 560 1.需要固定定义算法骨架,实现了一个算法的不变的部分,并把可变的行为留给子类来实现的情况。 561 2.各个子类中具有公共行为,应该抽取出来,集中在一个公共类去实现,从而避免代码重复。 562 3.需要控制子类扩展的情况。模板方法模式会在特定的点来调用子类的方法,这样只允许在这些点进行扩展。 563 */ 564 565 566 // http://blog.csdn.net/dead_of_winter/article/details/2159420 567 568 function parent(prototype) { 569 return function () { 570 for (var p in o) this[p] = prototype[p]; 571 // 模板方法 572 this.show = function () { 573 alert("show"); 574 } 575 }; 576 } 577 578 // 广度优先搜索的例子 579 580 function BreadthFirstSearch(extend, beam, finish) { 581 return function () { 582 this.finish = finish; 583 this.extend = extend; 584 this.beam = beam; 585 this.search = function () { 586 587 var queue = [this]; 588 while (queue.length) { 589 var current = queue.shift(); 590 if (!current.beam()) { 591 var extended = current.extend(); 592 for (var i = 0; i < extended.length; i++) { 593 if (extended[i].finish())return extended[i]; 594 queue.push(extended[i]); 595 } 596 } 597 } 598 return null; 599 } 600 } 601 } 602 603 604 (function () { 605 // 解决八皇后问题的例子的例子 606 607 function Queen(n) { 608 var ret = new Array(); 609 ret.size = n; //皇后问题的规模 610 ret.depth = 0; //搜索的深度 611 ret.pos = 0; //新皇后的水平位置 612 for (var y = 0; y < n; y++) { 613 ret.push([]); 614 for (var x = 0; x < n; x++) 615 ret[ret.length - 1].push(0); 616 } 617 function objectPrototypeClone() { 618 var tmp = function () { 619 }; 620 tmp.prototype = this; 621 return new tmp; 622 } 623 624 ret.clone = function () { 625 var r = objectPrototypeClone.call(this); 626 for (var i = 0; i < n; i++) { 627 r[i] = objectPrototypeClone.call(this[i]) 628 } 629 return r; 630 } 631 ret.toString = function () { 632 var str = ""; 633 for (var y = 0; y < n; y++) { 634 for (var x = 0; x < n; x++) 635 str += this[y][x] == 0 ? "○" : "★"; 636 str += " "; 637 } 638 return str; 639 } 640 return ret; 641 } 642 643 function extendQueen() { 644 var ret = new Array(); 645 if (this.depth == this.size)return ret; 646 for (var i = 0; i < this.size; i++) { 647 var current = this.clone(); 648 //alert(current.depth); 649 current[current.depth][i] = 1; 650 current.pos = i; 651 current.depth++; 652 ret.push(current); 653 } 654 return ret; 655 } 656 657 function beamQueen() { 658 var x, y; 659 if (this.depth == 0)return false; 660 if (this.depth == this.size)return true; 661 x = this.pos; 662 y = this.depth - 1; 663 while (--x >= 0 && --y >= 0) 664 if (this[y][x] != 0)return true; 665 666 x = this.pos; 667 y = this.depth - 1; 668 while (--y >= 0) 669 if (this[y][x] != 0)return true; 670 671 x = this.pos; 672 y = this.depth - 1; 673 while (--y >= 0 && ++x < this.size) { 674 if (this[y][x] != 0)return true; 675 } 676 return false; 677 } 678 679 function finishQueen() { 680 681 if (this.depth < this.size)return false; 682 x = this.pos; 683 y = this.depth - 1; 684 while (--x >= 0 && --y >= 0) 685 if (this[y][x] != 0)return false; 686 687 x = this.pos; 688 y = this.depth - 1; 689 while (--y >= 0) 690 if (this[y][x] != 0)return false; 691 692 x = this.pos; 693 y = this.depth - 1; 694 while (--y >= 0 && ++x < this.size) { 695 if (this[y][x] != 0)return false; 696 } 697 698 console.log(++count + ". " + this); 699 return false; 700 } 701 702 function BreadthFirstSearch(extend, beam, finish) { 703 return function () { 704 this.finish = finish; 705 this.extend = extend; 706 this.beam = beam; 707 this.search = function () { 708 709 var queue = [this]; 710 while (queue.length) { 711 var current = queue.shift(); 712 if (!current.beam()) { 713 var extended = current.extend(); 714 for (var i = 0; i < extended.length; i++) { 715 if (extended[i].finish())return extended[i]; 716 queue.push(extended[i]); 717 } 718 } 719 } 720 return null; 721 } 722 } 723 } 724 725 function BFSQueen(n) { 726 var ret = new Queen(n); 727 var BFS = new BreadthFirstSearch(extendQueen, beamQueen, finishQueen); 728 BFS.apply(ret); 729 return ret; 730 } 731 732 var queen = new BFSQueen(8); 733 var count = 0; 734 queen.search(); 735 }()); 736 </script> 737 </body> 738 </html>