前言
上一次写的日历插件基本完成,中间我和团队一个高手交流了一下,其实就是他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; 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() + ' '; 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 + ' '; 52 totalAmount += thisAmount; 53 } 54 result += 'amount owed is ' + thisAmount + ' '; 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() + ' '; 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 + ' '; 25 totalAmount += thisAmount; 26 } 27 result += 'amount owed is ' + thisAmount + ' '; 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() + ' '; 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 + ' '; totalAmount += thisAmount; } result += 'amount owed is ' + thisAmount + ' '; 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() + ' '; 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 + ' '; 68 totalAmount += thisAmount; 69 } 70 result += 'amount owed is ' + thisAmount + ' '; 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() + ' '; 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() + ' '; 20 totalAmount += each.getChange(); 21 } 22 result += 'amount owed is ' + totalAmount + ' '; 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() + ' '; 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() + ' '; totalAmount += each.getChange(); } result += 'amount owed is ' + each.getChange() + ' '; 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() + ' '; 21 } 22 result += 'amount owed is ' + this.getTotal() + ' '; 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() + ' '; 82 for (var i = 0, len = this._rentals.length; i < len; i++) { 83 each = this._rentals[i]; 84 result += each.getMovie().getTitle() + ':' + each.getCharge() + ' '; 85 } 86 result += 'amount owed is ' + this.getTotal() + ' '; 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() + ' '; for (var i = 0, len = this._rentals.length; i < len; i++) { each = this._rentals[i]; result += each.getMovie().getTitle() + ':' + each.getCharge() + ' '; } result += 'amount owed is ' + this.getTotal() + ' '; 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:作者这里使用了抽象类神马的,我思考下这里怎么写......
结语
好了,今天的学习暂时到此,下次我们就真的开始系统学习重构知识了。