前言
上一次写的日历插件基本完成,中间我和团队一个高手交流了一下,其实就是他code review我的代码了,最后我发现我之前虽然能完成交待下来的任务但是代码却不好看。
这个不好看,是由于各种原因就这样了,于是当时就想说重构下吧,但是任务一来就给放下了。
现在想来,就算真的要重构,但是也不一定知道如何重构,无论最近学习jquery代码还是其他其实都是为了在思想上有所提升而不一定是代码上
如何然自己的代码更优雅
如何让自己的程序可扩展性高
如何让自己的代码更可用
这些都是接下来需要解决的问题,学习一事如逆水行舟啊!所以我这里搞了一本《重构》一书,准备在好好学习一番。
关于插件
这个说是插件其实代码还是比较糟糕的,写到后面也没怎么思考了,这里暂且搞出来各位看看,等后面点《重构》学习结束了做一次重构吧!
由于是公司已经再用的代码,我这里就只贴js代码,CSS就不搞出来了,有兴趣的同学就自己看看吧,我这里截个图各位觉得有用就看看代码吧:
简单列表应用
触发change事件
这个东西就是第一列的变化第二个会跟着变,第二个变了第三个也会变,然后点击确定后会回调一个函数,并获得所选值。
不可选项
这个中当滑动到无效(灰色)的选项时,会重置为最近一个可选项
源代码
View Code
请使用手机/或者使用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
View Code
搬移“计算”代码
观察amountFor时,我们发现此处具有rental的信息,却没有customer的信息,所以这里有一个问题:
绝大多数情况,函数应该放在他使用的数据的所属对象内
所以amountFor其实应该放到rental中去,为了适应变化,就得去掉参数,并且我们这里讲函数名一并更改了
这里贴出完整的代码,各位自己看看
View Code
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
View Code
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写错了。。。。。。
View Code
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采用同样方法处理
View Code
PS:这里搞完了,我没有发现和多态有太多关系的东西啦。。。。。。于是,继续往下看吧
继承
PS:这里要用到继承,我们应该使用前面博客的方法,但是现在就随便搞下吧
我们为Movie建立三个子类
ChildrenMovie RegularMovie NewReleseaMovie
PS:作者这里使用了抽象类神马的,我思考下这里怎么写..
结语
好了,今天的学习暂时到此,下次我们就真的开始系统学习重构知识了。
本文转自叶小钗博客园博客,原文链接:http://www.cnblogs.com/yexiaochai/p/3344213.html,如需转载请自行联系原作者