淺談工作中的設計模式【單例、工廠、橋接、裝飾】

葉小釵發表於2015-02-14

前言

記得剛畢業的時候參加了一次校招面試,之前表現的很好,最後時面試官問我懂不懂設計模式,我說不懂,然後就進去了;後面又參加了某大公司的校招,開始表現還行,後面面試官問我懂不懂設計模式,我說懂(上次後補習了下),最後把工廠模式的程式碼背寫到了紙上,然後就沒有然後了......

現在回想起來當時有點傻有點天真,沒有幾十萬的程式碼量,沒有一定的經驗總結,居然敢說懂設計模式,這不是找抽麼?

現在經過幾年工作學習,倒是是時候系統的回憶下平時工作內容,看用到了什麼設計模式,權當總結。

小釵對設計模式的理解程度有限,文中不足之處請您拍磚。

物件導向的實現

設計模式便是物件導向的深入,物件導向的應用,所以類的實現是第一步:

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();

說實話,就站在前端的角度,以及我的視野來說,這個裝飾者其實不太實用,換個說法,這個裝飾者模式非常類似面向切口程式設計,就是在某一個點前做點事情,後做點事情,這個時候事件管道似乎更加合適。

結語

今天回顧了單例、工廠、橋接、裝飾者模式,我們後面再繼續,文中有何不足請您指教。

相關文章