浅谈工作中的设计模式【单例、工厂、桥接、装饰】

前言

记得刚毕业的时候参加了一次校招面试,之前表现的很好,最后时面试官问我懂不懂设计模式,我说不懂,然后就进去了;后面又参加了某大公司的校招,开始表现还行,后面面试官问我懂不懂设计模式,我说懂(上次后补习了下),最后把工厂模式的代码背写到了纸上,然后就没有然后了......

现在回想起来当时有点傻有点天真,没有几十万的代码量,没有一定的经验总结,居然敢说懂设计模式,这不是找抽么?

现在经过几年工作学习,倒是是时候系统的回忆下平时工作内容,看用到了什么设计模式,权当总结。

小钗对设计模式的理解程度有限,文中不足之处请您拍砖。

面向对象的实现

设计模式便是面向对象的深入,面向对象的应用,所以类的实现是第一步:

PS:这里依赖了underscore,各位自己加上吧。

 1 //window._ = _ || {};
 2 // 全局可能用到的变量
 3 var arr = [];
 4 var slice = arr.slice;
 5 /**
 6 * inherit方法,js的继承,默认为两个参数
 7 *
 8 * @param  {function} origin  可选,要继承的类
 9 * @param  {object}   methods 被创建类的成员,扩展的方法和属性
10 * @return {function}         继承之后的子类
11 */
12 _.inherit = function (origin, methods) {
13 
14   // 参数检测,该继承方法,只支持一个参数创建类,或者两个参数继承类
15   if (arguments.length === 0 || arguments.length > 2) throw '参数错误';
16 
17   var parent = null;
18 
19   // 将参数转换为数组
20   var properties = slice.call(arguments);
21 
22   // 如果第一个参数为类(function),那么就将之取出
23   if (typeof properties[0] === 'function')
24     parent = properties.shift();
25   properties = properties[0];
26 
27   // 创建新类用于返回
28   function klass() {
29     if (_.isFunction(this.initialize))
30       this.initialize.apply(this, arguments);
31   }
32 
33   klass.superclass = parent;
34 
35   // 父类的方法不做保留,直接赋给子类
36   // parent.subclasses = [];
37 
38   if (parent) {
39     // 中间过渡类,防止parent的构造函数被执行
40     var subclass = function () { };
41     subclass.prototype = parent.prototype;
42     klass.prototype = new subclass();
43 
44     // 父类的方法不做保留,直接赋给子类
45     // parent.subclasses.push(klass);
46   }
47 
48   var ancestor = klass.superclass && klass.superclass.prototype;
49   for (var k in properties) {
50     var value = properties[k];
51 
52     //满足条件就重写
53     if (ancestor && typeof value == 'function') {
54       var argslist = /^\s*function\s*\(([^\(\)]*?)\)\s*?\{/i.exec(value.toString())[1].replace(/\s/i, '').split(',');
55       //只有在第一个参数为$super情况下才需要处理(是否具有重复方法需要用户自己决定)
56       if (argslist[0] === '$super' && ancestor[k]) {
57         value = (function (methodName, fn) {
58           return function () {
59             var scope = this;
60             var args = [
61               function () {
62                 return ancestor[methodName].apply(scope, arguments);
63               }
64             ];
65             return fn.apply(this, args.concat(slice.call(arguments)));
66           };
67         })(k, value);
68       }
69     }
70 
71     //此处对对象进行扩展,当前原型链已经存在该对象,便进行扩展
72     if (_.isObject(klass.prototype[k]) && _.isObject(value) && (typeof klass.prototype[k] != 'function' && typeof value != 'fuction')) {
73       //原型链是共享的,这里处理逻辑要改
74       var temp = {};
75       _.extend(temp, klass.prototype[k]);
76       _.extend(temp, value);
77       klass.prototype[k] = temp;
78     } else {
79       klass.prototype[k] = value;
80     }
81 
82   }
83 
84   if (!klass.prototype.initialize)
85     klass.prototype.initialize = function () { };
86 
87   klass.prototype.constructor = klass;
88 
89   return klass;
90 };

使用测试:

 1 var Person = _.inherit({
 2     initialize: function(opts) {
 3         this.setOpts(opts);
 4     },
 5 
 6     setOpts: function (opts) {
 7         for(var k in opts) {
 8             this[k] = opts[k];
 9         }
10     },
11 
12     getName: function() {
13         return this.name;
14     },
15 
16     setName: function (name) {
17         this.name = name
18     }
19 });
20 
21 var Man = _.inherit(Person, {
22     initialize: function($super, opts) {
23         $super(opts);
24         this.sex = 'man';
25     },
26 
27     getSex: function () {
28         return this.sex;
29     }
30 });
31 
32 var Woman = _.inherit(Person, {
33     initialize: function($super, opts) {
34         $super(opts);
35         this.sex = 'women';
36     },
37 
38     getSex: function () {
39         return this.sex;
40     }
41 });
42 
43 var xiaoming = new Man({
44     name: '小明'
45 });
46 
47 var xiaohong = new Woman({
48     name: '小红'
49 });
xiaoming.getName()
"小明"
xiaohong.getName()
"小红"
xiaoming.getSex()
"man"
xiaohong.getSex()
"women"

单例模式(Singleton)

单列为了保证一个类只有一个实例,如果不存在便直接返回,如果存在便返回上一次的实例,其目的一般是为了资源优化。

javascript中实现单例的方式比较多,比较实用的是直接使用对象字面量:

1 var singleton = {
2     property1: "property1",
3     property2: "property2",
4     method1: function () {}
5 };

类实现是正统的实现,一般是放到类上,做静态方法:

在实际项目中,一般这个应用会在一些通用UI上,比如mask,alert,toast,loading这类组件,还有可能是一些请求数据的model,简单代码如下:

 1 //唯一标识,一般在amd模块中
 2 var instance = null;
 3 
 4 //js不存在多线程,这里是安全的
 5 var UIAlert = _.inherit({
 6     initialize: function(msg) {
 7         this.msg = msg;
 8     },
 9     setMsg: function (msg) {
10         this.msg = msg;
11     },
12     showMessage: function() {
13         console.log(this.msg);
14     }
15 });
16 
17 var m1 = new UIAlert('1');
18 m1.showMessage();//1
19 var m2 = new UIAlert('2');
20 m2.showMessage();//2
21 m1.showMessage();//1

 如所示,这个是一个简单的应用,如果稍作更改的话:

 1 //唯一标识,一般在amd模块中
 2 var instance = null;
 3 
 4 //js不存在多线程,这里是安全的
 5 var UIAlert = _.inherit({
 6     initialize: function(msg) {
 7         this.msg = msg;
 8     },
 9     setMsg: function (msg) {
10         this.msg = msg;
11     },
12     showMessage: function() {
13         console.log(this.msg);
14     }
15 });
16 UIAlert.getInstance = function () {
17     if (instance instanceof this) {
18         return instance;
19     } else {
20         return instance = new UIAlert(); //new this
21     }
22 }
23 
24 var m1 = UIAlert.getInstance();
25 m1.setMsg(1);
26 m1.showMessage();//1
27 var m2 = UIAlert.getInstance();
28 m2.setMsg(2);
29 m2.showMessage();//2
30 m1.showMessage();//2

如所示,第二次的改变影响了m1的值,因为他们的实例msg是共享的,这个便是一次单列的使用,而实际场景复杂得多。

以alert组件为例,他还会存在按钮,一个、两个或者三个,每个按钮事件回调不一样,一次设置后,第二次使用时各个事件也需要被重置,比如事件装在一个数组eventArr = []中,每次这个数组需要被清空重置,整个组件的dom结构也会重置,好像这个单例的意义也减小了,真实情况是这样的意义在于全站,特别是对于webapp的网站,只有一个UI dom的根节点,这个才是该场景的意义所在。

而对mask而言便不太适合全部做单例,以弹出层UI来说,一般都会带有一个mask组件,如果一个组件弹出后马上再弹出一个,第二个mask如果与第一个共享的话便不合适了,因为这个mask应该是各组件独享的。

单例在javascript中的应用更多的还是来划分命名空间,比如underscore库,比如以下场景:

① Hybrid桥接的代码

window.Hybrid = {};//存放所有Hybrid的参数

② 日期函数

window.DateUtil = {};//存放一些日期操作方法,比如将“2015年2月14日”这类字符串转换为日期对象,或者逆向转换

......

工厂模式(Factory)

工厂模式是一个比较常用的模式,介于javascript对象的不定性,其在前端的应用门槛更低。

工厂模式出现之初意在解决对象耦合问题,通过工厂方法,而不是new关键字实例化具体类,将所有可能的类的实例化集中在一起。

一个最常用的例子便是我们的Ajax模块:

 1 var XMLHttpFactory = {};
 2 var XMLHttpFactory.createXMLHttp = function() {
 3     var XMLHttp = null;
 4     if (window.XMLHttpRequest){
 5     XMLHttp = new XMLHttpRequest()
 6     }else if (window.ActiveXObject){
 7     XMLHttp = new ActiveXObject("Microsoft.XMLHTTP")
 8     }
 9     return XMLHttp;
10 }

使用工厂方法的前提是,产品类的接口需要一致,至少公用接口是一致的,比如我们这里有一个需求是这样的:

可以看到各个模块都是不一样的:

① 数据请求

② dom渲染,样式也有所不同

③ 事件交互

但是他们有一样是相同的:会有一个共同的事件点:

① create

② show

③ hide

所以我们的代码可以是这样的:

 1 var AbstractView = _.inherit({
 2     initialize: function() {
 3         this.wrapper = $('body');
 4         //事件管道,实例化时触发onCreate,show时候触发onShow......
 5         this.eventsArr = [];
 6     },
 7     show: function(){},
 8     hide: function (){}
 9 });
10 var SinaView = _.inherit(AbstractView, {
11 });
12 var BaiduView = _.inherit(AbstractView, {
13 });

每一个组件实例化只需要执行实例化操作与show操作即可,各个view的显示逻辑在自己的事件管道实现,真实的逻辑可能是这样的

 1 var ViewContainer = {
 2     SinaView: SinaView,
 3     BaiduView: BaiduView
 4 };
 5 var createView = function (view, wrapper) {
 6     //这里会有一些监测工作,事实上所有的view类应该放到一个单列ViewContainer中
 7     var ins = new ViewContainer[view + 'View'];
 8     ins.wrapper = wrapper;
 9     ins.show();
10 }
11 //数据库读出数据
12 var moduleInfo = ['Baidu', 'Sina', '...'];
13 
14 for(var i = 0, len = moduleInfo.length; i < len; i++){
15     createView(moduleInfo[i]);
16 }

如之前写的坦克大战,创建各自坦克工厂模式也是绝佳的选择,工厂模式暂时到此。

桥接模式(bridge)

桥接模式一个非常典型的使用便是在Hybrid场景中,native同事会给出一个用于桥接native与H5的模块,一般为bridge.js。

native与H5本来就是互相独立又互相变化的,如何在多个维度的变化中又不引入额外复杂度,这个时候bridge模式便派上了用场,使抽象部分与实现部分分离,各自便能独立变化。

这里另举一个应用场景,便是UI与其动画类,UI一般会有show的动作,通常便直接显示了出来,但是我们实际工作中需要的UI显示是:由下向上动画显示,由上向下动画显示等效果。

这个时候我们应该怎么处理呢,简单设计一下:

 1 var AbstractView = _.inherit({
 2     initialize: function () {
 3         //这里的dom其实应该由template于data组成,这里简化
 4         this.$el = $('<div style="display: none; position: absolute; left: 100px; top: 100px; border: 1px solid #000000;">组件</div>');
 5         this.$wrapper = $('body');
 6         this.animatIns = null;
 7     },
 8     show: function () {
 9         this.$wrapper.append(this.$el);
10         if(!this.animatIns) {
11             this.$el.show();
12         } else {
13             this.animatIns.animate(this.$el, function(){});
14         }
15         //this.bindEvents();
16     }
17 });
18 
19 var AbstractAnimate = _.inherit({
20     initialize: function () {
21     },
22     //override
23     animate: function (el, callback) {
24         el.show();
25         callback();
26     }
27 });
28 
29 
30 var UPToDwonAnimate = _.inherit(AbstractAnimate, {
31     animate: function (el, callback) {
32         //动画具体实现不予关注,这里使用zepto实现
33         el.animate({
34             'transform': 'translate(0, -250%)'
35         }).show().animate({
36             'transform': 'translate(0, 0)'
37         }, 200, 'ease-in-out', callback);
38     }
39 });
40 
41 
42 var UIAlert = _.inherit(AbstractView, {
43     initialize: function ($super, animateIns) {
44         $super();
45         this.$el = $('<div style="display: none; position: absolute; left: 100px; top: 200px; border: 1px solid #000000;">alert组件</div>');
46         this.animatIns = animateIns;
47     }
48 });
49 
50 var UIToast = _.inherit(AbstractView, {
51     initialize: function ($super, animateIns) {
52         $super();
53         this.animatIns = animateIns;
54     }
55 });
56 
57 var t = new UIToast(new UPToDwonAnimate);
58 t.show();
59 
60 var a = new UIAlert();
61 a.show();

这里组件对动画类库有依赖,但是各自又不互相影响(事实上还是有一定影响的,比如其中一些事件便需要动画参数触发),这个便是一个典型的桥接模式。

再换个方向理解,UI的css样式事实上也可以做到两套系统,一套dom结构一套皮肤库,但是这个实现上有点复杂,因为html不可分割,而动画功能这样处理却比较合适。

装饰者模式(decorator)

装饰者模式的意图是为一个对象动态的增加一些额外职责;是类继承的另外一种选择,一个是编译时候增加行为,一个是运行时候。

装饰者要求其实现与包装的对象统一,并做到过程透明,意味着可以用他来包装其他对象,而使用方法与原来一致。

一次逻辑的执行可以包含多个装饰对象,这里举个例子来说,在webapp中每个页面的view往往会包含一个show方法,而在我们的页面中我们可能会根据localsorage或者ua判断要不要显示下面广告条,效果如下:

那么这个逻辑应该如何实现呢?

 1 var View = _.inherit({
 2     initialize: function () {},
 3     show: function () {
 4         console.log('渲染基本页面');
 5     }
 6 });
 7 
 8 //广告装饰者
 9 var AdDecorator = _.inherit({
10     initialize: function (view) {
11         this.view = view;
12     },
13     show: function () {
14         this.view.show();
15         console.log('渲染广告区域');
16     }
17 });
18 
19 //基本使用
20 var v = new View();
21 v.show();
22 
23 //........ .满足一定条件...........
24 var d = new AdDecorator(v);
25 d.show();

说实话,就站在前端的角度,以及我的视野来说,这个装饰者其实不太实用,换个说法,这个装饰者模式非常类似面向切口编程,就是在某一个点前做点事情,后做点事情,这个时候事件管道似乎更加合适。

结语

今天回顾了单例、工厂、桥接、装饰者模式,我们后面再继续,文中有何不足请您指教。

posted on 2015-02-14 14:45  叶小钗  阅读(2574)  评论(3编辑  收藏  举报