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>

 

posted @ 2013-06-02 16:49  LukeLin  阅读(975)  评论(0编辑  收藏  举报