天天看點

【重構筆記04】重新組織資料(我好像明白了什麼是資料與行為分離)

關于繼承

因為在後續的章節中,我們經常會用到繼承相關的知識,是以在這裡在說一說javascript中的繼承問題

首先,我們來看一個後續會經常用到的方法,我們會用他來建立類

 1 var base = {};

 2 var slice = [].slice;

 3 base.Class = function (supClass, childAttr) {

 4     //若是第一個是類,便是繼承;如果第一個是對象,第二個參數無意義,便是建立一個類

 5     if (typeof supClass === 'object') {

 6         childAttr = supClass;

 7         supClass = function () { };

 8     }

 9     //建立臨時類,最後作為新類傳回,可能是繼承可能是新類

10     /***

11     這裡非常關鍵,為整個方法的入口,一定得看到初始化後,這裡會執行構造函數

12     ***/

13     var newClass = function () {

14         //每個類都會使用該函數,作為第一步初始化,告訴類有哪些屬性

15         this._propertys_ && this._propertys_();

16         //第二步初始化,相當于子類的構造函數,比較重要,初始化方法不一定會出現

17         this.init && this.init.applay(this, arguments);

18     };

19     //發生繼承關系,可能為空類

20     newClass.prototype = new supClass();

21 

22     //建立類必定會包含初始化函數,要麼繼承,如果沒繼承,這裡也會建立

23     var supInit = newClass.prototype.init || function () { };

24     //傳入的子對象可能包含他的初始化方法,如果有一定要使用,至于父類使用與否看子類心情

25     var childInit = childAttr.init || function () { };

26     //父類的properys方法便是指定會具有哪些屬性,一定會執行

27     var _supAttr = newClass.prototype._propertys_ || function () { };

28     //子類的初始化也一定會觸發,先執行父類再執行子類

29     var _childAttr = childAttr._propertys_ || function () { };

30 

31     //為建立類(可能繼承可能建立)初始化原型,上面的會重寫,沒有就不管他

32     for (var k in childAttr) {

33         childAttr.hasOwnProperty(k) && (newClass.prototype[k] = childAttr[k]);

34     }

35 

36     //處理繼承情況

37     if (arguments.length && arguments[0].prototype && arguments[0].prototype.init === supInit) {

38         //根據父類重寫建立類構造時會用到的方法

39         newClass.prototype.init = function () {

40             var scope = this;

41             var args = [function () {

42                 //第一個參數為父類的初始化函數,執行與否看子類心情

43                 supInit.apply(scope, arguments)

44             } ];

45             childInit.apply(scope, args.concat(slice.call(arguments)));

46         };

47     }

48     //前面說到的,父類與子類的初始化方法一定會執行,先父後子

49     newClass.prototype._propertys_ = function () {

50         _supAttr.call(this);

51         _childAttr.call(this);

52     };

53 

54     //成員屬性也得繼承

55     for (var k in supClass) {

56         supClass.hasOwnProperty(k) && (newClass[k] = supClass[k]);

57     }

58     return newClass;

59 };

現在,我們使用上述方法建立一個父類AbstractView,與兩個實際的View來說明繼承的重要

如果每個頁面都重新加載頁面的話,對我們來說毫無意義,說明繼承問題還是要單頁,于是我們實作一個簡單的MVC模式

簡單的MVC

首先是我們的Control

 1 var Control = base.Class({

 2     _propertys_: function () {

 3         this.body = $('body');

 4         this.isCreateViewPort = false;

 5         this.defaultView = 'Index';

 6         this.views = {};

 7         this.interface = {

 8             forward: bind(this, this.forward)

 9         };

10     },

11     init: function () {

12         this.loadView(this.defaultView, this.interface);

13     },

14     createViewPort: function () {

15         if (this.isCreateViewPort) return;

16         this.viewPort = $('<div id="viewPort"></div>');

17         this.body.append(this.viewPort);

18         this.isCreateViewPort = true;

19     },

20     loadView: function (viewName) {

21         this.createViewPort();

22         var v = this.views[viewName];

23         if (!v) {

24             var View = window[viewName];

25             v = new View(this.interface);

26             this.viewPort.append(v.root);

27             var events = v.getEvents();

28             this._addEvent(v.root, events);

29             this.views[viewName] = v;

30         }

31         for (var k in this.views) {

32             this.views[k].hide();

33         }

34         v.show();

35     },

36     forward: function (viewName) {

37         this.loadView(viewName);

38     },

39     _addEvent: function (root, events) {

40         root = root || this.body;

41         //事件綁定

42         for (var k in events) {

43             var k_t = k.split(',');

44             root.find(k_t[0]).on(k_t[1], bind(this, events[k]));

45         }

46     }

47 });

然後是我們的父級View

 1 var AbstractView = base.Class({

 2     init: function (interface) {

 3         //opts為0/false等情況不予考慮

 4         this.interface = interface;

 5         this.root = $('<div id="' + this.viewName + '"></div>');

 6 

 7     },

 8     onShow: function () {

 9         console.log('onshow');

11     show: function () {

12         this.onShow();

13         this.root.show()

14     },

15     onHide: function () {

16 

17         console.log('onHide');

18     },

19     hide: function () {

20         this.onHide();

21         this.root.hide();

22     },

23     forward: function (viewName) {

24         this.interface.forward(viewName);

25     }

26 });

以下是兩個子集View

 1 var Index = base.Class(AbstractView, {

 3         this.viewName = 'index';

 4     },

 5     init: function (supInit, interface) {

 6         supInit(interface);

 7         this.initDom();

 8     },

 9     initDom: function () {

10         //index模闆,此處我故意将id寫成一樣

11         var tmpt = [

12             '我是index<input type="button" id="goto" value="去清單頁">'

13         ].join('');

14         this.root.html(tmpt);

15     },

16     getEvents: function () {

17         return {

18             '#goto,click': function (e) {

19                 this.forward('List');

20             }

21         };

22     }

23 });

24 

25 var List = base.Class(AbstractView, {

26     _propertys_: function () {

27         this.viewName = 'list';

28     },

29     init: function (supInit, interface) {

30         supInit(interface);

31         this.initDom();

32     },

33     initDom: function () {

34         var tmpt = [

35             '我是list<input type="button" id="goto" value="回首頁">'

36         ].join('');

37         this.root.html(tmpt);

39     getEvents: function () {

40         return {

41             '#goto,click': function (e) {

42                 this.forward('Index');

43             }

44         };

45     }

46 });

然後我們調用後就有感覺了,請各位看demo

http://sandbox.runjs.cn/show/dg55tgdo

這個代碼雖然簡單,但是還是可以說明一些問題:

① 各個VIew獨立(index與list),友善各個操作,control作為總驅動,提供各種全局接口

② 由于都是繼承自AbstractView,比如我現在想在個view展示前執行一點邏輯判斷的話,隻需要在onload中

1 onShow: function () {

2 console.log('onshow');

3 console.log(this.viewName + '顯示前的一些邏輯');

4 }

因為這個代碼也沒有什麼特點,隻不過簡單模拟MVC了就不多說,在此希望能說清楚繼承的優點,于是繼續今天的話題吧

PS:今天後,後續代碼中,一旦與類有關,全部使用上述代碼了

自封裝字段

我們有時直接通路一個字段,但與字段直接的耦合關系會逐漸變的笨拙

這時為這個字段建立取值/設值的函數,并使用函數擷取字段也許是一不錯的選擇

 1 var _Class = function () {

 2     this.low;

 3     this.high;

 4 };

 5 _Class.prototype = {

 6     includes: function (arg) {

 7         return arg >= this.low && arg <= this.high;

 9 };

10 

11 //變成這個樣子

12 var _Class = function () {

13     this.low;

14     this.high;

15 };

16 _Class.prototype = {

17     includes: function (arg) {

18         return arg >= this.getLow() && arg <= this.getHigh();

20     getLow: function () {

21         //do something

22         return this.low;

23     },

24     getHigh: function () {

25         //do something

26         return this.high;

27     }

28 };

在字段通路方式上,有時我們認為将變量定義在類中,于是我們可以自由通路,但有時我們卻會間接通路

間接通路帶來的好處就是,子類可以複寫函數改變擷取資料的途徑,并且資料的管理方式更加靈活(js中就比較玄了)

我們可以先使用直接擷取方式,後面換為間接方式

怎麼做?

① 為待封裝字段建立取值/設定函數

② 找出該字段所有引用點,将他們替換為函數

③ 測試

 1 var InitRange = function (opts) {

 2     this.low = opts.low;

 3     this.high = opts.high;

 5 InitRange.prototype = {

 9     grow: function (factor) {

10         this.high = this.high * factor;

11     }

12 };

13 /*

14 ------看我重構--------

15 */

16 var InitRange = function (opts) {

17     this.low = opts.low;

18     this.high = opts.high;

19 };

20 InitRange.prototype = {

21     includes: function (arg) {

22         return arg >= this.getLow && arg <= this.getHigh;

24     grow: function (factor) {

25         this.setHigh(this.getHigh() * factor)

26     },

27     getLow: function () {

28         return this.low;

29     },

30     getHigh: function () {

31         return this.high;

33     setLow: function (arg) {

34         this.low = arg;

36     setHigh: function (arg) {

37         this.high = arg;

38     }

39 };

使用本重構時,我們需要注意“在構造函數中使用設定函數”的情況

一般來說,設值函數被認為應該在對象建立後再使用,是以初始化過程中的行為可能與設定函數的行為不同

這個時候就不要構造函數時候初始化了,這個時候如果我們使用了子類,優勢就比較明顯了

以對象取代資料值

我們有一個資料項,需要與其他資料和行為一起使用才有意義,那麼将資料變為對象

剛開始開發時,我們的資料代碼會比較簡單,但随着開發的進度,我們會發現資料不是那麼簡單了......

比如開始我們使用一個字元串來表示電話号碼,但是後面電話擁有格式化、區号等行為,那麼就該對象化了

來個例子說明一番,這裡有一個訂單的Order類

 1 var Odrer = function (opts) {

 2     this.customer = opts.customer

 3 };

 4 Odrer.prototype = {

 5     getCustomer: function () {

 6         return this.customer;

 8     setCustomer: function (args) {

 9         this.customer = args;

10     }

11 };

12 

13 //使用的代碼可能如下

14 function numberOfOrdersFor(orders, customer) {

15     var result = 0;

16     for (var k in orders) {

17         if (orders[k] == customer) result++;

18     }

19     return result;

20 }

22 var Odrer = function (opts) {

23     this.customer = opts.customer

24 };

25 Odrer.prototype = {

26     getCustomer: function () {

27         return this.customer;

29     setCustomer: function (args) {

30         this.customer = args;

31     }

32 };

33 

34 //使用的代碼可能如下

35 function numberOfOrdersFor(orders, customer) {

36     var result = 0;

37     for (var k in orders) {

38         if (orders[k].getCunstomer() == customer) result++;

39     }

40     return result;

41 }

42 

43 //這裡我們需要建立一個Customer的類

44 var Customer = function (name) {

45     this.name = name

46 };

47 Customer.prototype = {

48     getName: function () {

49         return this.name;

50     }

51 };

52 

53 //第二步,我們将order中的customer字段類型修改一番(js其實不存在類型概念)

54 var Odrer = function (opts) {

55     this.customer = new Customer(opts.customer);

56 };

57 Odrer.prototype = {

58     getCustomer: function () {

59         return this.customer.getName();

60     },

61     setCustomer: function (args) {

62         this.customer = new Customer(args);

63     }

64 };

65 

66 //第四步就将用到的地方改了

67 function numberOfOrdersFor(orders, customer) {

68     var result = 0;

69     for (var k in orders) {

70         if (orders[k].getCustomer() == customer) result++;

71     }

72     return result;

73 }

值對象改為引用對象/引用對象改為值對象

我們從一個類衍生出許多彼此相等的執行個體時,我們會希望他隻是一個對象

在許多系統中,我們可以對對象做一個分類:引用對象和值對象

前者像客戶、賬戶這樣的東西,每個對象都代表真實世界中的一個實物,可以直接使用==來檢測相等

後者就像日期、錢這樣的東西,他完全由其所含資料值來定義,不在意副本的存在

PS:周一搞了一個通宵的班,感覺今天狀态不行啊。。。。。

指派被監視資料

我們有一些資料在頁面UI中,而領域函數也需要通路這些資料

那麼将資料指派到一個領域對象中,建立一個Observer模式,用以同步領域對象和UI對象的重複資料

一個分層良好的系統,應該将處理使用者界面和處理業務代碼分開!!!

因為我們可能需要使用不同的使用者界面來表現相同的業務,這是絕對會發生的,如果同一界面承擔兩種責任,那麼使用者界面就複雜了

而且,與UI分離後,領域對象的維護和演化将會更加容易,我們甚至可以讓不同的開發者負責不同部分的開發

盡管可以輕松将行為劃分到不同部位,資料卻不能如此。同一項資料可能既内嵌于UI中又儲存于領域模型中

PS:以前端來說就是既在标簽中,又在函數中(MVC很大程度就是解決這個問題)

如果遇到代碼以兩層方式開發,業務邏輯内嵌于使用者界面中,就應該将行為分離出來了

至此我終于知道原來自己說了一句行為與資料分離是什麼意思了!!!

主要工作就是函數的分離和搬遷,移動資料即是将資料移往對象,并且提供資料同步機制(感覺有點MVVM了)

① 修改現有類,使其成為領域類的Observer

如果沒有領域類就建立一個/如果沒有“從展現類到領域類”的關聯,就将領域類儲存于展現類的一個字段中

② 針對UI中的資料使用自封裝字段處理

③ 在事件處理函數中調用設值函數,更新UI元件

在事件處理函數中放一個設值函數,利用他将UI元件更新為記憶體中的值,雖然隻是拿他的值為他設定而已,但是這樣的設值函數便是允許其中任何動作得以于日後被執行起來的意義所在

進行這個改變時,對于元件,不要使用取值函數,應該直接取用,因為我們後面會修改取值函數,讓他從領域對象取值

④ 在領域類中定于資料及其相關通路函數

確定領域類中的設值函數能幹觸發觀察者模式的通信機制

對于被觀察的資料,在領域中使用與展現類所用的相同類型(一般是字元串)來儲存

⑤ 修改展現類的通路函數,将他們的操作對象改為領域對象

⑥ 修改觀察者的update方法,使其從相應的領域對象中将所需資料複制給UI

這個類容比較重要,于是我們來個例子吧:

例子比較簡單,當使用者修改文本中的數字時,另外兩個文本框中的文本框自動更新

修改start則length改變,修改length則end改變

首先,我們所有函數都寫在一起:

 1 <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">

 2 <html xmlns="http://www.w3.org/1999/xhtml">

 3 <head>

 4     <title></title>

 5 </head>

 6 <body>

 7     start:<input type="text" id="start" value="4" /><br />

 8     end:<input type="text" id="end" value="12"/><br />

 9     length:<input type="text" id="length" value="8" />

10 </body>

11 <script src="../jquery-1.7.1.js" type="text/javascript"></script>

12 <script type="text/javascript">

13     var start = $('#start');

14     var end = $('#end');

15     var len = $('#length');

17     //為每個input綁定失去焦點事件

18     start.blur(calculateLength);

19     end.blur(calculateLength);

20     len.blur(calculateEnd);

22     function calculateLength() {

23         len.val(parseInt(end.val()) - parseInt(start.val()));

24     }

25     function calculateEnd() {

26         end.val(parseInt(len.val()) + parseInt(start.val()));

28 </script>

29 </html>

這個代碼其實沒什麼問題,但是我們現在要把與展現無關的邏輯從UI剔除(對前端來說有點難)

說白了就是将calculate*類代碼提出來,于是我們需要在頁面外使用start等文本框的值,是以這些資料也會被複制,并且需要與UI保持同步

這裡我們先建立一個領域類,讓他與我們的page産生關聯,作為資料傳輸的樞紐,然後再将具體的運算放到其它地方

  1 <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">

  2 <html xmlns="http://www.w3.org/1999/xhtml">

  3 <head>

  4     <title></title>

  5 </head>

  6 <body>

  7     start:<input type="text" id="start" value="4" /><br />

  8     end:<input type="text" id="end" value="12"/><br />

  9     length:<input type="text" id="len" value="8" />

 10     <h3>變形</h3>

 11     <div id="change"></div>

 12 </body>

 13 <script src="../jquery-1.7.1.js" type="text/javascript"></script>

 14 <script type="text/javascript">

 15     var slice = [].slice;

 16     var bind = function (scope, fun, args) {

 17         args = args || [];

 18         return function () {

 19             fun.apply(scope, args.concat(slice.call(arguments)));

 20         };

 21     };

 22     var M = function () {

 23         this.start = 0;

 24         this.end = 0;

 25         this.len = 0;

 26     };

 27     M.prototype = {

 28         getStart: function () {

 29             return this.start;

 30         },

 31         setStart: function (args) {

 32             this.start = args;

 33         },

 34         getEnd: function () {

 35             return this.end;

 36         },

 37         setEnd: function (args) {

 38             this.end = args;

 39         },

 40         getLen: function () {

 41             return this.len;

 42         },

 43         setLen: function (args) {

 44             this.len = args;

 45         },

 46         calculateLength: function () {

 47             //此處可以加入其它邏輯

 48             var len = parseInt(this.end) - parseInt(this.start);

 49             this.setLen(len);

 50         },

 51         calculateEnd: function (m) {

 52             var end = parseInt(this.len) + parseInt(this.start)

 53             this.setEnd(end);

 54         }

 55     };

 56 

 57 

 58 var V = function (s, e, l) {

 59     this.start = s;

 60     this.end = e;

 61     this.len = l;

 62     this.m ;

 63 };

 64 V.prototype = {

 65     setM: function (args) {

 66         this.m = args;

 67         this.m.setStart(this.start.val());

 68         this.m.setEnd(this.end.val());

 69         this.m.setLen(this.len.val());

 70     },

 71     focusLost: function () {

 72         this.start.blur(bind(this, this.start_blur));

 73         this.end.blur(bind(this, this.end_blur));

 74         this.len.blur(bind(this, this.len_blur));

 75 

 76     },

 77     start_blur: function () {

 78         this.m.setStart(this.start.val());

 79         this.calculateLength();

 80     },

 81     end_blur: function () {

 82         this.m.setEnd(this.end.val());

 83         this.calculateLength();

 84     },

 85     len_blur: function () {

 86         this.m.setLen(this.len.val());

 87         this.calculateEnd();

 88     },

 89     calculateLength: function () {

 90         this.m.calculateLength();

 91         this.update();

 92 

 93     },

 94     calculateEnd: function () {

 95         this.m.calculateEnd();

 96         this.update();

 97 

 98     },

 99     update: function () {

100         var s1 = this.m.getStart();

101         var s2 = this.m.getEnd();

102         var s3 = this.m.getLen();

103 

104         this.start.val(this.m.getStart());

105         this.end.val(this.m.getEnd());

106         this.len.val(this.m.getLen());

107     }

108 };

109 

110 var v1 = new V($('#start'), $('#end'), $('#len'));

111 var m1 = new M();

112 v1.setM(m1);

113 v1.focusLost();

114 

115 //這樣做的好處就是我可以輕易變形

116 //下面的重複代碼我就不管了

117 var change = $('#change');

118     var s1 = $('<select id="s1"></select>');

119     var s2 = $('<select id="s2"></select>');

120     var s3 = $('<select id="s3"></select>');

121     change.append(s1);

122     change.append(s2);

123     change.append(s3);

124 

125     for (var i = -100; i < 100; i++) {

126         s1.append($('<option>' + i + '</option>'));

127         s2.append($('<option>' + i + '</option>'));

128         s3.append($('<option>' + i + '</option>'));

129     }

130 

131     var v2 = new V($('#s1'), $('#s2'), $('#s3'));

132     var m2 = new M();

133     v2.setM(m2);

134     v2.focusLost();

135 </script>

136 </html>

demo位址:

http://sandbox.runjs.cn/show/bjsc6zui

本文轉自葉小钗部落格園部落格,原文連結http://www.cnblogs.com/yexiaochai/p/3397767.html如需轉載請自行聯系原作者

繼續閱讀