浅谈工作中的设计模式【单例、工厂、桥接、装饰】
前言
记得刚毕业的时候参加了一次校招面试,之前表现的很好,最后时面试官问我懂不懂设计模式,我说不懂,然后就进去了;后面又参加了某大公司的校招,开始表现还行,后面面试官问我懂不懂设计模式,我说懂(上次后补习了下),最后把工厂模式的代码背写到了纸上,然后就没有然后了......
现在回想起来当时有点傻有点天真,没有几十万的代码量,没有一定的经验总结,居然敢说懂设计模式,这不是找抽么?
现在经过几年工作学习,倒是是时候系统的回忆下平时工作内容,看用到了什么设计模式,权当总结。
小钗对设计模式的理解程度有限,文中不足之处请您拍砖。
面向对象的实现
设计模式便是面向对象的深入,面向对象的应用,所以类的实现是第一步:
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();
说实话,就站在前端的角度,以及我的视野来说,这个装饰者其实不太实用,换个说法,这个装饰者模式非常类似面向切口编程,就是在某一个点前做点事情,后做点事情,这个时候事件管道似乎更加合适。
结语
今天回顾了单例、工厂、桥接、装饰者模式,我们后面再继续,文中有何不足请您指教。