前言
上一次寫的日歷插件基本完成,中間我和團隊一個高手交流了一下,其實就是他code review我的代碼了,最后我發現我之前雖然能完成交待下來的任務但是代碼卻不好看。
這個不好看,是由於各種原因就這樣了,於是當時就想說重構下吧,但是任務一來就給放下了。
現在想來,就算真的要重構,但是也不一定知道如何重構,無論最近學習jquery代碼還是其他其實都是為了在思想上有所提升而不一定是代碼上
如何然自己的代碼更優雅
如何讓自己的程序可擴展性高
如何讓自己的代碼更可用
這些都是接下來需要解決的問題,學習一事如逆水行舟啊!所以我這里搞了一本《重構》一書,准備在好好學習一番。
關於插件
這個說是插件其實代碼還是比較糟糕的,寫到后面也沒怎么思考了,這里暫且搞出來各位看看,等后面點《重構》學習結束了做一次重構吧!
由於是公司已經再用的代碼,我這里就只貼js代碼,CSS就不搞出來了,有興趣的同學就自己看看吧,我這里截個圖各位覺得有用就看看代碼吧:
簡單列表應用
觸發change事件
這個東西就是第一列的變化第二個會跟着變,第二個變了第三個也會變,然后點擊確定后會回調一個函數,並獲得所選值。
不可選項
這個中當滑動到無效(灰色)的選項時,會重置為最近一個可選項
源代碼

1 var ScrollList = function (opts) { 2 3 //兼容性方案處理,以及后期資源清理 4 var isTouch = 'ontouchstart' in document.documentElement; 5 isTouch = true; 6 this.start = isTouch ? 'touchstart' : 'mousedown'; 7 this.move = isTouch ? 'touchmove' : 'mousemove'; 8 this.end = isTouch ? 'touchend' : 'mouseup'; 9 this.startFn; 10 this.moveFn; 11 this.endFn; 12 13 opts = opts || {}; 14 15 //數據源 16 this.data = opts.data || []; 17 this.dataK = {}; //以id作為檢索鍵值 18 19 this.initBaseDom(opts); 20 21 this._changed = opts.changed || null; 22 //當選情況下會有一個初始值 23 this.selectedIndex = parseInt(this.disItemNum / 2); //暫時不考慮多選的情況 24 if (this.type == 'list') { 25 this.selectedIndex = -1; 26 } 27 this.selectedIndex = opts.index != undefined ? opts.index : this.selectedIndex; 28 29 //如果數組長度有問題的話 30 this.selectedIndex = this.selectedIndex > this.data.length ? 0 : this.selectedIndex; 31 32 var isFind = false, index = this.selectedIndex; 33 if (this.data[index] && (typeof this.data[index].disabled == 'undefined' || this.data[index].disabled == false)) { 34 for (i = index, len = this.data.length; i < len; i++) { 35 if (typeof this.data[i].disabled == 'undefined' || this.data[i].disabled == true) { 36 index = i; 37 isFind = true; 38 break; 39 } 40 } 41 if (isFind == false) { 42 for (i = index; i != 0; i--) { 43 if (typeof this.data[i].disabled == 'undefined' || this.data[i].disabled == true) { 44 index = i; 45 isFind = true; 46 break; 47 } 48 } 49 } 50 if (isFind) this.selectedIndex = index; 51 } 52 53 this.animateParam = opts.animateParam || [50, 40, 30, 25, 20, 15, 10, 8, 6, 4, 2]; //動畫參數 54 this.animateParam = opts.animateParam || [10, 8, 6, 5, 4, 3, 2, 1, 0, 0, 0]; //動畫參數 55 56 this.setBaseParam(); 57 this.init(); 58 }; 59 ScrollList.prototype = { 60 constructor: ScrollList, 61 init: function () { 62 this.initItem(); 63 this.wrapper.append(this.body); 64 this.initEventParam(); 65 this.bindEvent(); 66 this.setIndex(this.selectedIndex, true); 67 }, 68 //基本參數設置 69 setBaseParam: function () { 70 /* 71 定位實際需要用到的信息 72 暫時不考慮水平移動吧 73 */ 74 this.setHeight = 0; //被設置的高度 75 this.itemHeight = 0; //單個item高度 76 this.dragHeight = 0; //拖動元素高度 77 this.dragTop = 0; //拖動元素top 78 this.timeGap = 0; //時間間隔 79 this.touchTime = 0; //開始時間 80 this.moveAble = false; //是否正在移動 81 this.moveState = 'up'; //移動狀態,up right down left 82 this.oTop = 0; //拖動前的top值 83 this.curTop = 0; //當前容器top 84 this.mouseY = 0; //鼠標第一次點下時相對父容器的位置 85 this.cooling = false; //是否處於冷卻時間 86 }, 87 initBaseDom: function (opts) { 88 //容器元素 89 this.wrapper = opts.wrapper || $(document); 90 this.type = opts.type || 'list'; //list, radio 91 92 //顯示的項目,由此確定顯示區域的高度,所以height無用 93 this.disItemNum = 5; 94 95 var id = opts.id || 'id_' + new Date().getTime(); 96 var className = opts.className || 'cui-roller-bd'; 97 98 var scrollClass; 99 //單選的情況需要確定顯示選擇項 100 if (this.type == 'list') { 101 scrollClass = 'cui-select-view'; 102 } 103 else if (this.type == 'radio') { 104 scrollClass = 'ul-list'; 105 this.disItemNum = 3; 106 } 107 this.disItemNum = opts.disItemNum || this.disItemNum; 108 this.disItemNum = this.disItemNum % 2 == 0 ? this.disItemNum + 1 : this.disItemNum; //必須是奇數 109 110 scrollClass = opts.scrollClass || scrollClass; 111 this.scrollClass = scrollClass; 112 113 //這里使用height不還有待商榷,因為class含有樣式 114 115 this.body = $([ 116 '<div class="' + className + '" style="overflow: hidden; position: relative; " id="' + id + '" >', 117 '</div>' 118 ].join('')); 119 //真正拖動的元素(現在是ul) 120 this.dragEl = $([ 121 '<ul class="' + scrollClass + '" style="position: absolute; width: 100%;">', 122 '</ul>' 123 ].join('')); 124 this.body.append(this.dragEl); 125 //單選情況需要加入蒙版 126 // if (this.type == 'radio' && this.disItemNum != 1) { 127 // this.body.append($([ 128 // '<div class="cui-mask"></div>', 129 // '<div class="cui-lines"> </div>' 130 // ].join(''))); 131 // } 132 }, 133 //增加數據 134 initItem: function () { 135 var _tmp, _data, i, k, val; 136 this.size = this.data.length; //當前容量 137 for (var i in this.data) { 138 _data = this.data[i]; 139 _data.index = i; 140 141 if (typeof _data.key == 'undefined') _data.key = _data.id; 142 if (typeof _data.val == 'undefined') _data.val = _data.name; 143 144 145 val = _data.val || _data.key; 146 this.dataK[_data.key] = _data; 147 _tmp = $('<li>' + val + '</li>'); 148 _tmp.attr('data-index', i); 149 if (typeof _data.disabled != 'undefined' && _data.disabled == false) { 150 _tmp.css('color', 'gray'); 151 } 152 153 this.dragEl.append(_tmp); 154 } 155 156 }, 157 //初始化事件需要用到的參數信息 158 initEventParam: function () { 159 //如果沒有數據的話就在這里斷了吧 160 if (this.data.constructor != Array || this.data.length == 0) return false; 161 var offset = this.dragEl.offset(); 162 var li = this.dragEl.find('li').eq(0); 163 var itemOffset = li.offset(); 164 //暫時不考慮邊框與外邊距問題 165 this.itemHeight = itemOffset.height; 166 this.setHeight = this.itemHeight * this.disItemNum; 167 this.body.css('height', this.setHeight); 168 this.dragTop = offset.top; 169 this.dragHeight = this.itemHeight * this.size; 170 var s = ''; 171 }, 172 bindEvent: function () { 173 var scope = this; 174 this.startFn = function (e) { 175 scope.touchStart.call(scope, e); 176 }; 177 this.moveFn = function (e) { 178 scope.touchMove.call(scope, e); 179 }; 180 this.endFn = function (e) { 181 scope.touchEnd.call(scope, e); 182 }; 183 184 this.dragEl[0].addEventListener(this.start, this.startFn, false); 185 this.dragEl[0].addEventListener(this.move, this.moveFn, false); 186 this.dragEl[0].addEventListener(this.end, this.endFn, false); 187 }, 188 removeEvent: function () { 189 this.dragEl[0].removeEventListener(this.start, this.startFn); 190 this.dragEl[0].removeEventListener(this.move, this.moveFn); 191 this.dragEl[0].removeEventListener(this.end, this.endFn); 192 }, 193 touchStart: function (e) { 194 var scope = this; 195 //冷卻時間不能開始 196 if (this.cooling) { 197 setTimeout(function () { 198 scope.cooling = false; 199 }, 500); 200 return false; 201 } 202 //需要判斷是否是拉取元素,此處需要遞歸驗證,這里暫時不管 203 //!!!!!!!!此處不嚴謹 204 var el = $(e.target).parent(), pos; 205 if (el.hasClass(this.scrollClass)) { 206 this.touchTime = e.timeStamp; 207 //獲取鼠標信息 208 pos = this.getMousePos((e.changedTouches && e.changedTouches[0]) || e); 209 //注意,此處是相對位置,注意該處還與動畫有關,所以高度必須動態計算 210 //可以設置一個冷卻時間參數,但想想還是算了 211 //最后還是使用了冷卻時間 212 //最后的最后我還是決定使用動態樣式獲取算了 213 var top = parseFloat(this.dragEl.css('top')) || 0; 214 this.mouseY = pos.top - top; 215 // this.mouseY = pos.top - this.curTop; 216 this.moveAble = true; 217 } 218 }, 219 touchMove: function (e) { 220 if (!this.moveAble) return false; 221 var pos = this.getMousePos((e.changedTouches && e.changedTouches[0]) || e); 222 //先獲取相對容器的位置,在將兩個鼠標位置相減 223 this.curTop = pos.top - this.mouseY; 224 this.dragEl.css('top', this.curTop + 'px'); 225 e.preventDefault(); 226 }, 227 touchEnd: function (e) { 228 if (!this.moveAble) return false; 229 this.cooling = true; //開啟冷卻時間 230 231 //時間間隔 232 var scope = this; 233 this.timeGap = e.timeStamp - this.touchTime; 234 var flag = this.oTop <= this.curTop ? 1 : -1; //判斷是向上還是向下滾動 235 var flag2 = this.curTop > 0 ? 1 : -1; //這個會影響后面的計算結果 236 this.moveState = flag > 0 ? 'up' : 'down'; 237 var ih = parseFloat(this.itemHeight); 238 var ih1 = ih / 2; 239 240 var top = Math.abs(this.curTop); 241 var mod = top % ih; 242 top = (parseInt(top / ih) * ih + (mod > ih1 ? ih : 0)) * flag2; 243 var step = parseInt(this.timeGap / 10 - 10); 244 245 step = step > 0 ? step : 0; 246 var speed = this.animateParam[step] || 0; 247 var increment = speed * ih * flag; 248 top += increment; 249 250 //!!!此處動畫可能導致數據不同步,后期改造需要加入冷卻時間 251 if (this.oTop != this.curTop && this.curTop != top) { 252 this.dragEl.animate({ 253 top: top + 'px' 254 }, 100 + (speed * 20), 'ease-out', function () { 255 // scope.curTop = top; 256 scope.reset.call(scope, top); 257 }); 258 } else { 259 var item = this.dragEl.find('li'); 260 var el = $(e.target); 261 item.removeClass('current'); 262 el.addClass('current'); 263 264 //這個由於使用了邊距等東西,使用位置定位有點不靠譜了 265 this.selectedIndex = el.attr('data-index'); 266 //單選多選列表觸發的事件,反正都會觸發 267 this.type == 'list' && this.onTouchEnd(); 268 this.cooling = false; //關閉冷卻時間 269 } 270 this.moveAble = false; 271 e.preventDefault(); 272 273 }, 274 //超出限制后位置還原 275 reset: function (top) { 276 var scope = this; 277 var num = parseInt(scope.type == 'list' ? 0 : scope.disItemNum / 2); 278 var _top = top, t = false; 279 280 var sHeight = scope.type == 'list' ? 0 : parseFloat(scope.itemHeight) * num; 281 var eHeight = scope.type == 'list' ? scope.setHeight : parseFloat(scope.itemHeight) * (num + 1); 282 var h = this.dragHeight; 283 284 if (top >= 0) { 285 if (top > sHeight) { 286 _top = sHeight; 287 t = true; 288 } else { 289 //出現該情況說明項目太少,達不到一半 290 if (h <= sHeight) { 291 _top = sHeight - scope.itemHeight * (this.size - 1); 292 t = true; 293 } 294 } 295 } 296 if (top < 0 && (top + scope.dragHeight <= eHeight)) { 297 t = true; 298 _top = (scope.dragHeight - eHeight) * (-1); 299 } 300 if (top == _top) { 301 t = false; 302 } 303 if (t) { 304 scope.dragEl.animate({ 305 top: _top + 'px' 306 }, 50, 'ease-in-out', function () { 307 scope.oTop = _top; 308 scope.curTop = _top; 309 scope.cooling = false; //關閉冷卻時間 310 //單選時候的change事件 311 scope.type == 'radio' && scope.onTouchEnd(); 312 }); 313 } else { 314 scope.oTop = top; 315 scope.curTop = top; 316 //單選時候的change事件 317 scope.type == 'radio' && scope.onTouchEnd(); 318 } 319 scope.cooling = false; //關閉冷卻時間 320 }, 321 onTouchEnd: function (scope) { 322 scope = scope || this; 323 324 var secItem, i, len, index, isFind; 325 var changed = this._changed; 326 var num = parseInt(this.type == 'list' ? 0 : this.disItemNum / 2); 327 len = this.data.length; 328 if (this.type == 'radio') { 329 i = parseInt((this.curTop - this.itemHeight * num) / parseFloat(this.itemHeight)); 330 this.selectedIndex = Math.abs(i); 331 secItem = this.data[this.selectedIndex]; 332 } else { 333 secItem = this.data[this.selectedIndex]; 334 } 335 336 //默認不去找 337 isFind = false; //檢測是否找到可選項 338 //檢測是否當前項不可選,若是不可選,需要還原到最近一個可選項 339 if (typeof secItem.disabled != 'undefined' && secItem.disabled == false) { 340 index = this.selectedIndex; 341 //先向上計算 342 if (this.moveState == 'up') { 343 for (i = index; i != 0; i--) { 344 if (typeof this.data[i].disabled == 'undefined' || this.data[i].disabled == true) { 345 index = i; 346 isFind = true; 347 break; 348 } 349 } 350 if (isFind == false) { 351 for (i = index; i < len; i++) { 352 if (typeof this.data[i].disabled == 'undefined' || this.data[i].disabled == true) { 353 index = i; 354 isFind = true; 355 break; 356 } 357 } 358 } 359 } else { 360 for (i = index; i < len; i++) { 361 if (typeof this.data[i].disabled == 'undefined' || this.data[i].disabled == true) { 362 index = i; 363 isFind = true; 364 break; 365 } 366 } 367 if (isFind == false) { 368 for (i = index; i != 0; i--) { 369 if (typeof this.data[i].disabled == 'undefined' || this.data[i].disabled == true) { 370 index = i; 371 isFind = true; 372 break; 373 } 374 } 375 } 376 } 377 } 378 379 //會有還原的邏輯 380 if (isFind) { 381 this.selectedIndex = index; 382 this.setIndex(index); 383 } else { 384 var changed = this._changed; 385 if (changed && typeof changed == 'function') { 386 changed.call(scope, secItem); 387 } 388 } 389 }, 390 //數據重新加載 391 reload: function (data) { 392 393 this.data = data; 394 this.dragEl.html(''); 395 if (data.constructor == Array && data.length > 0) { 396 this.selectedIndex = parseInt(this.disItemNum / 2); //暫時不考慮多選的情況 397 this.selectedIndex = this.selectedIndex > this.data.length ? this.data.length - 1 : this.selectedIndex; 398 this.initItem(); 399 this.initEventParam(); 400 this.cooling = false; 401 this.setIndex(this.selectedIndex, true); 402 } 403 }, 404 setKey: function (k) { 405 if (k == undefined || k == null) return false; 406 var i = this.dataK[k] && this.dataK[k].index; 407 this.setIndex(i); 408 }, 409 setIndex: function (i, init) { 410 if (i == undefined || i < 0) return false; 411 var scope = this; 412 // this.cooling = true; //關閉冷卻時間 413 var num = parseInt(scope.disItemNum / 2); 414 415 if (scope.type == 'list') { 416 num = i == 0 ? 0 : 1; 417 } 418 419 var i = parseInt(i), top; 420 if (i < 0) return false; 421 if (i >= this.data.length) i = this.data.length - 1; 422 this.selectedIndex = i; 423 top = (i * this.itemHeight * (-1) + this.itemHeight * num); 424 425 //防止設置失敗 426 scope.oTop = top; 427 scope.curTop = top; 428 scope.cooling = false; //關閉冷卻時間 429 // scope.dragEl.css('top', top + 'px'); 430 431 scope.dragEl.animate({ 'top': top + 'px' }, 50, 'ease-in-out'); 432 433 434 if (scope.type == 'list') { 435 var item = scope.dragEl.find('li'); 436 item.removeClass('current'); 437 item.eq(i).addClass('current'); 438 } 439 //初始化dom選項時不觸發事件 440 if (!init) { 441 //單選時候的change事件 442 scope.onTouchEnd(); 443 } 444 }, 445 getSelected: function () { 446 return this.data[this.selectedIndex]; 447 }, 448 getByKey: function (k) { 449 var i = this.dataK[k] && this.dataK[k].index; 450 if (i != null && i != undefined) 451 return this.data[i]; 452 return null; 453 }, 454 //獲取鼠標信息 455 getMousePos: function (event) { 456 var top, left; 457 top = Math.max(document.body.scrollTop, document.documentElement.scrollTop); 458 left = Math.max(document.body.scrollLeft, document.documentElement.scrollLeft); 459 return { 460 top: top + event.clientY, 461 left: left + event.clientX 462 }; 463 } 464 }; 465 return ScrollList;
請使用手機/或者使用chrome開啟touch功能查看,最新js代碼已處理兼容性問題
http://sandbox.runjs.cn/show/prii13pm
總結
代碼沒來得及重構,各位將就下吧,接下來進入我們的重構學習!
重構第一步
簡單程序
原來作者使用java寫的,我這里用js實現可能有所不同,如果有問題請提出
首先我們跟着學習第一個例子,實例據說比較簡單,是一個影片出租店用的程序,計算每一個顧客的消費金額並打印詳情。
操作者告訴程序,影片分為三類:普通片/兒童片/租期多長,程序便根據租賃時間和影片類型計算費用,並且為常客計算積分
PS:然后作者畫了個圖,我們不去管他
Movie(影片)
1 //影片,單純的數據類 2 var Movie = function (title, priceCode) { 3 this._title = title; 4 this._priceCode = priceCode; 5 6 }; 7 Movie.CHILDRENS = 2; 8 Movie.REGULAR = 0; 9 Movie.NEW_RELEASE = 1; 10 11 Movie.prototype = { 12 constructor: Movie, 13 getPriceCode: function () { 14 return this._priceCode; 15 }, 16 setPriceCode: function (arg) { 17 this._priceCode = arg; 18 }, 19 getTitle: function () { 20 return this._title; 21 } 22 };
租賃
1 //租賃 2 var Rental = function (movie, daysRented) { 3 this._movie = movie; 4 this._daysRented = daysRented; 5 }; 6 7 Rental.prototype = { 8 constructor: Rental, 9 getDaysRented: function () { 10 return this._daysRented; 11 }, 12 getMovie: function () { 13 return this._movie; 14 } 15 };
顧客
PS:這里用到了Vector
,但是我們用數組代替吧
1 var Customer = function (name) { 2 this._name = name; 3 this._rentals = []; 4 }; 5 Customer.prototype = { 6 constructor: Customer, 7 addRental: function (arg) { 8 //加入的是一個rental實例 9 this._rentals.push(arg); 10 11 }, 12 getName: function () { 13 return this._name; 14 }, 15 //生成詳細訂單的函數,並擁有交互代碼 16 statement: function () { 17 var totalAmount = 0, 18 //積分 19 frequentRenterPoints = 0, 20 //原文為枚舉類型 21 rentals = this._rentals, 22 result = 'rental record for ' + this.getName() + '\n'; 23 24 var i, 25 thisAmount = 0, 26 each = null, 27 28 len = rentals.length; 29 30 //PS:尼瑪兩年不搞java了,這里居然有點讀不懂了。。。 31 //這里大概是要遍歷rentals的意思,所以代碼我給變了點 32 for (i = 0; i < len; i++) { 33 thisAmount = 0; 34 each = rentals[i]; 35 switch (each.getMovie().getPriceCode()) { 36 case Movie.REGULAR: 37 thisAmount += 2; 38 if (each.getDaysRented() > 2) thisAmount += (each.getDaysRented() - 2) * 1.5; 39 break; 40 case Movie.NEW_RELEASE: 41 thisAmount += each.getDaysRented() * 3; 42 break; 43 case Movie.CHILDRENS: 44 thisAmount += 1.5; 45 if (each.getDaysRented() > 3) thisAmount += (each.getDaysRented() - 3) * 1.5; 46 break; 47 } 48 frequentRenterPoints++; 49 if ((each.getMovie().getPriceCode() == Movie.NEW_RELEASE) && each.getDaysRented() > 1) frequentRenterPoints++; 50 51 result += each.getMovie().getTitle() + ':' + thisAmount + '\n'; 52 totalAmount += thisAmount; 53 } 54 result += 'amount owed is ' + thisAmount + '\n'; 55 result += 'you earned ' + frequentRenterPoints; 56 return result; 57 } 58 };
先試試程序吧
1 //此處先做一個例子試試吧 2 var m1 = new Movie('刀戟戡魔錄', 0); 3 var m2 = new Movie('霹靂神州', 1); 4 var m3 = new Movie('開疆記', 2); 5 6 var r1 = new Rental(m1, 1); 7 var r2 = new Rental(m2, 2); 8 var r3 = new Rental(m3, 3); 9 10 var y = new Customer('葉小釵'); 11 12 y.addRental(r1); 13 y.addRental(r2); 14 y.addRental(r3); 15 16 alert(y.statement());
程序總結
PS:這里完全就算調用作者的話了,老夫到此除了認識到對java忘得差不多了,沒有其他感受......
該程序具有以下特點:
① 不符合面向對象精神
② statement過長(這個我是真的感覺很長,我打了很久字)
③ 擴展性差
以上如果用戶希望對系統做一點修改,比如希望用html輸出,我們就發現statement整個就是一個2B了,於是我們一般會復雜粘貼一番(趕時間的情況至少我會這么做)
這樣一來也許多了一個htmlStatement的函數,但是大量重復的代碼,我是不能接受的,以下是一個因素:
如果計費標准發生變化了我們就需要修改代碼!而且是維護兩端代碼(讀到這,老夫感受很深啊),所以這里還可能帶來潛在威脅哦!
於是現在來了第二個變化:
用戶希望改變影片分類規則,但又不知道怎么改,他設想了幾種方案,這些方案都會影響計算方式,那么又應該如何呢??
PS:尼瑪這簡直是我們工作真正的寫照啊!老板/產品 想要一個方案,但是又不知道想要神馬!於是我們一般說的是這個不能實現(其實我們知道是可以實現的)
綜上,你知道為什么要重構了嗎?
至於你知不知道,反正我知道了。。。。。。
分解重組
測試
開始之前,作者大力強調了一下測試與建立單元測試的重要性,而且第四章會講,我這里先不糾結啦:)
分解重組statement
第一步,我們需要將長得離譜的statement干掉,代碼越小越簡單,代碼越小越少BUG
於是我們首先要找出代碼的邏輯泥團,並運用extract method,至於本例,邏輯泥團就是switch語句,我們將它提煉成單獨的函數
我們提煉一個函數時,我們要知道自己可能出什么錯,提煉不好就可能引入BUG
PS:這種情況也經常在工作中出現,我改一個BUG,結果由於新的代碼引起其它BUG!!!
提煉函數
找出在代碼中的局部變量,這里是each與thisAmount,前者未變,后者會變
任何不會改變的變量都可以被當成參數傳入新的函數,至於需要改變的變量就需要格外小心
如果只有一個變量會被修改,我們可以將它作為返回值
thisAmount是個臨時變量,每次循環都會被初始化為0 ,並且在switch以前不會被修改,所以我們可以將它作為返回值使用
重構的代碼
1 statement: function () { 2 var totalAmount = 0, 3 //積分 4 frequentRenterPoints = 0, 5 //原文為枚舉類型 6 rentals = this._rentals, 7 result = 'rental record for ' + this.getName() + '\n'; 8 9 var i, 10 thisAmount = 0, 11 each = null, 12 13 len = rentals.length; 14 15 //PS:尼瑪兩年不搞java了,這里居然有點讀不懂了。。。 16 //這里大概是要遍歷rentals的意思,所以代碼我給變了點 17 for (i = 0; i < len; i++) { 18 thisAmount = 0; 19 each = rentals[i]; 20 thisAmount = this._amountFor(each); 21 frequentRenterPoints++; 22 if ((each.getMovie().getPriceCode() == Movie.NEW_RELEASE) && each.getDaysRented() > 1) frequentRenterPoints++; 23 24 result += each.getMovie().getTitle() + ':' + thisAmount + '\n'; 25 totalAmount += thisAmount; 26 } 27 result += 'amount owed is ' + thisAmount + '\n'; 28 result += 'you earned ' + frequentRenterPoints; 29 return result; 30 }, 31 _amountFor: function (each) { 32 var thisAmount = 0; 33 switch (each.getMovie().getPriceCode()) { 34 case Movie.REGULAR: 35 thisAmount += 2; 36 if (each.getDaysRented() > 2) thisAmount += (each.getDaysRented() - 2) * 1.5; 37 break; 38 case Movie.NEW_RELEASE: 39 thisAmount += each.getDaysRented() * 3; 40 break; 41 case Movie.CHILDRENS: 42 thisAmount += 1.5; 43 if (each.getDaysRented() > 3) thisAmount += (each.getDaysRented() - 3) * 1.5; 44 break; 45 } 46 return thisAmount; 47 }
這里雖說只是做了一點改變,但是明顯代碼質量有所提升,然后內部的變量名也可以改變,比如:
① each => rental
② thisAmount => result

_amountFor: function (rental) { var result = 0; switch (rental.getMovie().getPriceCode()) { case Movie.REGULAR: result += 2; if (rental.getDaysRented() > 2) result += (rental.getDaysRented() - 2) * 1.5; break; case Movie.NEW_RELEASE: result += rental.getDaysRented() * 3; break; case Movie.CHILDRENS: result += 1.5; if (rental.getDaysRented() > 3) result += (rental.getDaysRented() - 3) * 1.5; break; } return result; }
搬移“計算”代碼
觀察amountFor時,我們發現此處具有rental的信息,卻沒有customer的信息,所以這里有一個問題:
絕大多數情況,函數應該放在他使用的數據的所屬對象內
所以amountFor其實應該放到rental中去,為了適應變化,就得去掉參數,並且我們這里講函數名一並更改了
這里貼出完整的代碼,各位自己看看

var Movie = function (title, priceCode) { this._title = title; this._priceCode = priceCode; }; Movie.CHILDRENS = 2; Movie.REGULAR = 0; Movie.NEW_RELEASE = 1; Movie.prototype = { constructor: Movie, getPriceCode: function () { return this._priceCode; }, setPriceCode: function (arg) { this._priceCode = arg; }, getTitle: function () { return this._title; } }; //租賃 var Rental = function (movie, daysRented) { this._movie = movie; this._daysRented = daysRented; }; Rental.prototype = { constructor: Rental, getDaysRented: function () { return this._daysRented; }, getMovie: function () { return this._movie; }, getChange: function () { var result = 0; switch (this.getMovie().getPriceCode()) { case Movie.REGULAR: result += 2; if (this.getDaysRented() > 2) result += (this.getDaysRented() - 2) * 1.5; break; case Movie.NEW_RELEASE: result += this.getDaysRented() * 3; break; case Movie.CHILDRENS: result += 1.5; if (this.getDaysRented() > 3) result += (this.getDaysRented() - 3) * 1.5; break; } return result; } }; //顧客 var Customer = function (name) { this._name = name; this._rentals = []; }; Customer.prototype = { constructor: Customer, addRental: function (arg) { //加入的是一個rental實例 this._rentals.push(arg); }, getName: function () { return this._name; }, //生成詳細訂單的函數,並擁有交互代碼 statement: function () { var totalAmount = 0, //積分 frequentRenterPoints = 0, //原文為枚舉類型 rentals = this._rentals, result = 'rental record for ' + this.getName() + '\n'; var i, thisAmount = 0, each = null, len = rentals.length; //PS:尼瑪兩年不搞java了,這里居然有點讀不懂了。。。 //這里大概是要遍歷rentals的意思,所以代碼我給變了點 for (i = 0; i < len; i++) { thisAmount = 0; each = rentals[i]; thisAmount = this._amountFor(each); frequentRenterPoints++; if ((each.getMovie().getPriceCode() == Movie.NEW_RELEASE) && each.getDaysRented() > 1) frequentRenterPoints++; result += each.getMovie().getTitle() + ':' + thisAmount + '\n'; totalAmount += thisAmount; } result += 'amount owed is ' + thisAmount + '\n'; result += 'you earned ' + frequentRenterPoints; return result; }, _amountFor: function (rental) { return rental.getChange(); } }; //此處先做一個例子試試吧 var m1 = new Movie('刀戟戡魔錄', 0); var m2 = new Movie('霹靂神州', 1); var m3 = new Movie('開疆記', 2); var r1 = new Rental(m1, 1); var r2 = new Rental(m2, 2); var r3 = new Rental(m3, 3); var y = new Customer('葉小釵'); y.addRental(r1); y.addRental(r2); y.addRental(r3); alert(y.statement());
1 Rental.prototype = { 2 constructor: Rental, 3 getDaysRented: function () { 4 return this._daysRented; 5 }, 6 getMovie: function () { 7 return this._movie; 8 }, 9 getChange: function () { 10 var result = 0; 11 switch (this.getMovie().getPriceCode()) { 12 case Movie.REGULAR: 13 result += 2; 14 if (this.getDaysRented() > 2) result += (this.getDaysRented() - 2) * 1.5; 15 break; 16 case Movie.NEW_RELEASE: 17 result += this.getDaysRented() * 3; 18 break; 19 case Movie.CHILDRENS: 20 result += 1.5; 21 if (this.getDaysRented() > 3) result += (this.getDaysRented() - 3) * 1.5; 22 break; 23 } 24 return result; 25 } 26 }; 27 28 //顧客 29 var Customer = function (name) { 30 this._name = name; 31 this._rentals = []; 32 }; 33 Customer.prototype = { 34 constructor: Customer, 35 addRental: function (arg) { 36 //加入的是一個rental實例 37 this._rentals.push(arg); 38 39 }, 40 getName: function () { 41 return this._name; 42 }, 43 //生成詳細訂單的函數,並擁有交互代碼 44 statement: function () { 45 var totalAmount = 0, 46 //積分 47 frequentRenterPoints = 0, 48 //原文為枚舉類型 49 rentals = this._rentals, 50 result = 'rental record for ' + this.getName() + '\n'; 51 52 var i, 53 thisAmount = 0, 54 each = null, 55 56 len = rentals.length; 57 58 //PS:尼瑪兩年不搞java了,這里居然有點讀不懂了。。。 59 //這里大概是要遍歷rentals的意思,所以代碼我給變了點 60 for (i = 0; i < len; i++) { 61 thisAmount = 0; 62 each = rentals[i]; 63 thisAmount = each.getChange(); 64 frequentRenterPoints++; 65 if ((each.getMovie().getPriceCode() == Movie.NEW_RELEASE) && each.getDaysRented() > 1) frequentRenterPoints++; 66 67 result += each.getMovie().getTitle() + ':' + thisAmount + '\n'; 68 totalAmount += thisAmount; 69 } 70 result += 'amount owed is ' + thisAmount + '\n'; 71 result += 'you earned ' + frequentRenterPoints; 72 return result; 73 } 74 };
去除多余變量
於是,現在statement中就有一些多余的變量了:this.Amount,因為他完全等於each.getCharge()
於是乎,去掉吧:
1 statement: function () { 2 var totalAmount = 0, 3 //積分 4 frequentRenterPoints = 0, 5 //原文為枚舉類型 6 rentals = this._rentals, 7 result = 'rental record for ' + this.getName() + '\n'; 8 9 var i, 10 each = null, 11 len = rentals.length; 12 //PS:尼瑪兩年不搞java了,這里居然有點讀不懂了。。。 13 //這里大概是要遍歷rentals的意思,所以代碼我給變了點 14 for (i = 0; i < len; i++) { 15 each = rentals[i]; 16 frequentRenterPoints++; 17 if ((each.getMovie().getPriceCode() == Movie.NEW_RELEASE) && each.getDaysRented() > 1) frequentRenterPoints++; 18 19 result += each.getMovie().getTitle() + ':' + each.getChange() + '\n'; 20 totalAmount += each.getChange(); 21 } 22 result += 'amount owed is ' + totalAmount + '\n'; 23 result += 'you earned ' + frequentRenterPoints; 24 return result; 25 }
提煉“常客積分”計算
下面開始對常客積分計算進行處理,積分的計算因為種類而有所不同,看來有理由把積分計算的責任放入rental

var Movie = function (title, priceCode) { this._title = title; this._priceCode = priceCode; }; Movie.CHILDRENS = 2; Movie.REGULAR = 0; Movie.NEW_RELEASE = 1; Movie.prototype = { constructor: Movie, getPriceCode: function () { return this._priceCode; }, setPriceCode: function (arg) { this._priceCode = arg; }, getTitle: function () { return this._title; } }; //租賃 var Rental = function (movie, daysRented) { this._movie = movie; this._daysRented = daysRented; }; Rental.prototype = { constructor: Rental, getDaysRented: function () { return this._daysRented; }, getMovie: function () { return this._movie; }, getChange: function () { var result = 0; switch (this.getMovie().getPriceCode()) { case Movie.REGULAR: result += 2; if (this.getDaysRented() > 2) result += (this.getDaysRented() - 2) * 1.5; break; case Movie.NEW_RELEASE: result += this.getDaysRented() * 3; break; case Movie.CHILDRENS: result += 1.5; if (this.getDaysRented() > 3) result += (this.getDaysRented() - 3) * 1.5; break; } return result; }, getFrequentRenterPoints: function () { if ((this.getMovie().getPriceCode() == Movie.NEW_RELEASE) && this.getDaysRented() > 1) return 2; else return 1; } }; //顧客 var Customer = function (name) { this._name = name; this._rentals = []; }; Customer.prototype = { constructor: Customer, addRental: function (arg) { //加入的是一個rental實例 this._rentals.push(arg); }, getName: function () { return this._name; }, //生成詳細訂單的函數,並擁有交互代碼 statement: function () { var totalAmount = 0, //積分 frequentRenterPoints = 0, //原文為枚舉類型 rentals = this._rentals, result = 'rental record for ' + this.getName() + '\n'; var i, each = null, len = rentals.length; //PS:尼瑪兩年不搞java了,這里居然有點讀不懂了。。。 //這里大概是要遍歷rentals的意思,所以代碼我給變了點 for (i = 0; i < len; i++) { each = rentals[i]; /* 重構掉的 frequentRenterPoints++; if ((each.getMovie().getPriceCode() == Movie.NEW_RELEASE) && each.getDaysRented() > 1) frequentRenterPoints++; */ frequentRenterPoints += each.getFrequentRenterPoints(); result += each.getMovie().getTitle() + ':' + each.getChange() + '\n'; totalAmount += each.getChange(); } result += 'amount owed is ' + each.getChange() + '\n'; result += 'you earned ' + frequentRenterPoints; return result; } }; //此處先做一個例子試試吧 var m1 = new Movie('刀戟戡魔錄', 0); var m2 = new Movie('霹靂神州', 1); var m3 = new Movie('開疆記', 2); var r1 = new Rental(m1, 1); var r2 = new Rental(m2, 2); var r3 = new Rental(m3, 3); var y = new Customer('葉小釵'); y.addRental(r1); y.addRental(r2); y.addRental(r3); alert(y.statement());
PS:由於篇幅較長,我就不像上面一一標注改變啦
下面再去除一點臨時變量:totalAmount
PS:但是,這里會多一次循環,到底哪個好,我也不知道了,多一個循環應該方便后面擴展吧,感覺作者要消滅所有臨時變量啦
去除totalAmount/frequentRenterPoints
1 var Customer = function (name) { 2 this._name = name; 3 this._rentals = []; 4 }; 5 Customer.prototype = { 6 constructor: Customer, 7 addRental: function (arg) { 8 //加入的是一個rental實例 9 this._rentals.push(arg); 10 11 }, 12 getName: function () { 13 return this._name; 14 }, 15 //生成詳細訂單的函數,並擁有交互代碼 16 statement: function () { 17 var each = null, result = ''; 18 for (var i = 0, len = this._rentals.length; i < len; i++) { 19 each = this._rentals[i]; 20 result += each.getMovie().getTitle() + ':' + each.getChange() + '\n'; 21 } 22 result += 'amount owed is ' + this.getTotal() + '\n'; 23 result += 'you earned ' + this.getTotalFrequentRenterPoints(); 24 return result; 25 }, 26 getTotal: function () { 27 var result = 0, each = null; 28 for (var i = 0, len = this._rentals.length; i < len; i++) { 29 each = this._rentals[i]; 30 result += each.getChange(); 31 } 32 return result; 33 }, 34 getTotalFrequentRenterPoints: function () { 35 var result = 0, each = null; 36 for (var i = 0, len = this._rentals.length; i < len; i++) { 37 each = this._rentals[i]; 38 result += each.getFrequentRenterPoints(); 39 } 40 return result; 41 } 42 };
請各位仔細看,到這里我們的程序已經變話了許多了!!!你還記得最初的statement嗎?
階段總結
可以看到,我們這次重構沒有減少代碼,反而加了很多代碼!而且還可能多了些循環呢!所以這次重構的結果是:
① 代碼易讀性提高
② 分離了statement
③ 代碼增多
④ 性能降低
在此看來,可能因為1,2我們便不做重構了,但是
不能因為:
① 重構增加了代碼量
② 重構降低了性能
而不做重構,因為重構完成前,這些只是你的一廂情願
添加htmlStatement
1 htmlStatement: function () { 2 var each = null, 3 result = '<h1>rental record for ' + this.getName() + '</h1>'; 4 for (var i = 0, len = this._rentals.length; i < len; i++) { 5 each = this._rentals[i]; 6 result += each.getMovie().getTitle() + ':' + each.getChange() + '<br/>'; 7 } 8 result += 'amount owed is ' + this.getTotal() + '<br/>'; 9 result += 'you earned ' + this.getTotalFrequentRenterPoints(); 10 return result; 11 },
多態與if
好了,用戶提出新需求了,需要修改分類規則。
這里我們又重新回到了我們的switch語句,我其實一般不使用switch語句,作者說最好不要在另一個對象屬性繼承上運用switch語句,要用也要在自己的數據上,而我基本不用。。。。。。
所以第一步,我們是將getCharge放入Movie中
getCharge搬家
PS:我怕好像將getCharge寫錯了。。。。。。

1 var Movie = function (title, priceCode) { 2 this._title = title; 3 this._priceCode = priceCode; 4 5 }; 6 Movie.CHILDRENS = 2; 7 Movie.REGULAR = 0; 8 Movie.NEW_RELEASE = 1; 9 10 Movie.prototype = { 11 constructor: Movie, 12 getPriceCode: function () { 13 return this._priceCode; 14 }, 15 setPriceCode: function (arg) { 16 this._priceCode = arg; 17 }, 18 getTitle: function () { 19 return this._title; 20 }, 21 getCharge: function (daysRented) { 22 var result = 0; 23 switch (this.getPriceCode()) { 24 case Movie.REGULAR: 25 result += 2; 26 if (daysRented > 2) result += (daysRented - 2) * 1.5; 27 break; 28 case Movie.NEW_RELEASE: 29 result += daysRented * 3; 30 break; 31 case Movie.CHILDRENS: 32 result += 1.5; 33 if (daysRented > 3) result += (daysRented - 3) * 1.5; 34 break; 35 } 36 return result; 37 } 38 }; 39 40 //租賃 41 var Rental = function (movie, daysRented) { 42 this._movie = movie; 43 this._daysRented = daysRented; 44 }; 45 46 Rental.prototype = { 47 constructor: Rental, 48 getDaysRented: function () { 49 return this._daysRented; 50 }, 51 getMovie: function () { 52 return this._movie; 53 }, 54 getCharge: function () { 55 return this.getMovie().getCharge(this.getDaysRented()); 56 }, 57 getFrequentRenterPoints: function () { 58 if ((this.getMovie().getPriceCode() == Movie.NEW_RELEASE) && this.getDaysRented() > 1) return 2; 59 else return 1; 60 } 61 }; 62 63 //顧客 64 var Customer = function (name) { 65 this._name = name; 66 this._rentals = []; 67 }; 68 Customer.prototype = { 69 constructor: Customer, 70 addRental: function (arg) { 71 //加入的是一個rental實例 72 this._rentals.push(arg); 73 74 }, 75 getName: function () { 76 return this._name; 77 }, 78 //生成詳細訂單的函數,並擁有交互代碼 79 statement: function () { 80 var each = null, 81 result = 'rental record for ' + this.getName() + '\n'; 82 for (var i = 0, len = this._rentals.length; i < len; i++) { 83 each = this._rentals[i]; 84 result += each.getMovie().getTitle() + ':' + each.getCharge() + '\n'; 85 } 86 result += 'amount owed is ' + this.getTotal() + '\n'; 87 result += 'you earned ' + this.getTotalFrequentRenterPoints(); 88 return result; 89 }, 90 htmlStatement: function () { 91 var each = null, 92 result = '<h1>rental record for ' + this.getName() + '</h1>'; 93 for (var i = 0, len = this._rentals.length; i < len; i++) { 94 each = this._rentals[i]; 95 result += each.getMovie().getTitle() + ':' + each.getCharge() + '<br/>'; 96 } 97 result += 'amount owed is ' + this.getTotal() + '<br/>'; 98 result += 'you earned ' + this.getTotalFrequentRenterPoints(); 99 return result; 100 }, 101 getTotal: function () { 102 var result = 0, each = null; 103 for (var i = 0, len = this._rentals.length; i < len; i++) { 104 each = this._rentals[i]; 105 result += each.getCharge(); 106 } 107 return result; 108 }, 109 getTotalFrequentRenterPoints: function () { 110 var result = 0, each = null; 111 for (var i = 0, len = this._rentals.length; i < len; i++) { 112 each = this._rentals[i]; 113 result += each.getFrequentRenterPoints(); 114 } 115 return result; 116 } 117 };
Movie
1 getCharge: function (daysRented) { 2 var result = 0; 3 switch (this.getPriceCode()) { 4 case Movie.REGULAR: 5 result += 2; 6 if (daysRented > 2) result += (daysRented - 2) * 1.5; 7 break; 8 case Movie.NEW_RELEASE: 9 result += daysRented * 3; 10 break; 11 case Movie.CHILDRENS: 12 result += 1.5; 13 if (daysRented > 3) result += (daysRented - 3) * 1.5; 14 break; 15 } 16 return result; 17 }
Rental
1 getCharge: function () { 2 return this.getMovie().getCharge(this.getDaysRented()); 3 },
getFrequentRenterPoints采用同樣方法處理

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <html xmlns="http://www.w3.org/1999/xhtml"> <head> <title></title> <script type="text/javascript"> //影片,單純的數據類 var Movie = function (title, priceCode) { this._title = title; this._priceCode = priceCode; }; Movie.CHILDRENS = 2; Movie.REGULAR = 0; Movie.NEW_RELEASE = 1; Movie.prototype = { constructor: Movie, getPriceCode: function () { return this._priceCode; }, setPriceCode: function (arg) { this._priceCode = arg; }, getTitle: function () { return this._title; }, getCharge: function (daysRented) { var result = 0; switch (this.getPriceCode()) { case Movie.REGULAR: result += 2; if (daysRented > 2) result += (daysRented - 2) * 1.5; break; case Movie.NEW_RELEASE: result += daysRented * 3; break; case Movie.CHILDRENS: result += 1.5; if (daysRented > 3) result += (daysRented - 3) * 1.5; break; } return result; }, getFrequentRenterPoints: function (daysRented) { if ((this.getPriceCode() == Movie.NEW_RELEASE) && daysRented > 1) return 2; else return 1; } }; //租賃 var Rental = function (movie, daysRented) { this._movie = movie; this._daysRented = daysRented; }; Rental.prototype = { constructor: Rental, getDaysRented: function () { return this._daysRented; }, getMovie: function () { return this._movie; }, getCharge: function () { return this.getMovie().getCharge(this.getDaysRented()); }, getFrequentRenterPoints: function () { return this.getMovie().getFrequentRenterPoints(this.getDaysRented()); } }; //顧客 var Customer = function (name) { this._name = name; this._rentals = []; }; Customer.prototype = { constructor: Customer, addRental: function (arg) { //加入的是一個rental實例 this._rentals.push(arg); }, getName: function () { return this._name; }, //生成詳細訂單的函數,並擁有交互代碼 statement: function () { var each = null, result = 'rental record for ' + this.getName() + '\n'; for (var i = 0, len = this._rentals.length; i < len; i++) { each = this._rentals[i]; result += each.getMovie().getTitle() + ':' + each.getCharge() + '\n'; } result += 'amount owed is ' + this.getTotal() + '\n'; result += 'you earned ' + this.getTotalFrequentRenterPoints(); return result; }, htmlStatement: function () { var each = null, result = '<h1>rental record for ' + this.getName() + '</h1>'; for (var i = 0, len = this._rentals.length; i < len; i++) { each = this._rentals[i]; result += each.getMovie().getTitle() + ':' + each.getCharge() + '<br/>'; } result += 'amount owed is ' + this.getTotal() + '<br/>'; result += 'you earned ' + this.getTotalFrequentRenterPoints(); return result; }, getTotal: function () { var result = 0, each = null; for (var i = 0, len = this._rentals.length; i < len; i++) { each = this._rentals[i]; result += each.getCharge(); } return result; }, getTotalFrequentRenterPoints: function () { var result = 0, each = null; for (var i = 0, len = this._rentals.length; i < len; i++) { each = this._rentals[i]; result += each.getFrequentRenterPoints(); } return result; } }; //此處先做一個例子試試吧 var m1 = new Movie('刀戟戡魔錄', 0); var m2 = new Movie('霹靂神州', 1); var m3 = new Movie('開疆記', 2); var r1 = new Rental(m1, 1); var r2 = new Rental(m2, 2); var r3 = new Rental(m3, 3); var y = new Customer('葉小釵'); y.addRental(r1); y.addRental(r2); y.addRental(r3); window.onload = function () { document.getElementById('d').innerHTML = y.htmlStatement(); }; </script> </head> <body> <div id="d"></div> </body> </html>
PS:這里搞完了,我沒有發現和多態有太多關系的東西啦。。。。。。於是,繼續往下看吧
繼承
PS:這里要用到繼承,我們應該使用前面博客的方法,但是現在就隨便搞下吧
我們為Movie建立三個子類
ChildrenMovie RegularMovie NewReleseaMovie
PS:作者這里使用了抽象類神馬的,我思考下這里怎么寫......
結語
好了,今天的學習暫時到此,下次我們就真的開始系統學習重構知識了。