【重构笔记02】重新组织函数

简介:
前言

重构过程中,还是有一定标准可循的,每个重构手法有如下五个部分:

首先是名称(name),建造一个重构词汇表,名称是非常重要的 然后是一个简短概要,介绍重构手法适用的场景,以及他干的事情,这样我们可以快速找到所需重构方法

然后,介绍为什么需要这个重构,或者什么情况下适用这个重构做法,简明扼要的介绍如何一步步重构

最后,以一个十分简单的例子说明此重构如何运作

所以今天我们进入重构的学习吧!

提炼函数

我们重构是,重头戏就是处理函数,以js而言,函数重要性更是终于“类”的概念。

如何恰当的包装代码,如何减少过长的代码,这是我们多数时刻需要思考的。

但是要消除函数过长是不易的,一个函数过长说明这个函数所完成的业务很复杂,而且可能关联性很高,要将这样的代码拆分,就不止是重构的事情了

在此提炼函数变得十分考验一个人的水平,如何将一段代码从原先函数提取出来,如何将一个单数调用替换为函数本体,这些都不简单

最后改完,时常发现提炼的某些函数实际意义不大,我们还得考虑如何回溯原来的函数

难在何处

提炼函数不易,难在处理局部变量,临时变量尤其突出

处理一个函数时,我们可以先使用查询取代变量的方法取代临时变量

如果一个临时变量多次使用,可以使用分解临时变量的方法将它变得容易替换

但是,多数时候临时变量确实混乱,难以替换,这时候我们可以使用以函数对象取代函数的方法,这样的代价就会引入新类

参数带来的问题比临时变量少一点,前提是不在函数内为他赋值(不对参数赋值,对js来说就是一个传说,因为我们队参数赋值可以保证程序更健壮),但是移除对象赋值,也许能带给你不一样的感受

说了这么多,我们来好好审视下,我们的一些手段吧!!!

光说无码不行,我们先上一个例子,我们现在将这段代码放到一个独立的函数中,注意函数名需要解释函数用途哦

 1 var log = function (msg) { console.log(msg); };
 2 
 3 var printOwing = function (amount) {
 4     printBanner();
 5     log('name:' + _name);
 6     log('amount:' + _amount);
 7 };
 8 
 9 var printOwing = function (amount) {
10     printBanner();
11     printDetails(amount)
12 };
13 
14 var printDetails = function (amount) {
15     log('name:' + _name);
16     log('amount:' + _amount)
17 };
这是比较常见的重构手法,当我们看到一个过长的函数或者一段需要注释才能看懂的代码时,这段代码可能就需要放进独立的函数了

如果每个函数的粒度都很小,那么函数被复用的机会就大,这样高层函数看上去就像被函数名注释似的,这样函数复写也相对简单

如何做?

① 创造一个新函数,根据这个函数的意图来对它命名(以它“做神马”来命名,而不是以它"怎么做"命名)

PS:即使我们要提炼的代码非常简单,哪怕只是一个消息或者一个函数调用,只要新函数能更好的表示代码意图,就可以提炼,否则就不要动他了

② 将提炼的代码拷贝到新建函数中

③ 检查提炼的代码,看看其中是否引用了“作用域限于原函数”的变量(局部变量、原函数参数)

④ 检查是否包含“仅用于被提炼代码段”的临时变量,如果有,在目标函数中将之声明为局部变量

⑤ 检查被提炼代码段,看看是否有任何局部变量的值被他改变,如果一个临时变量的值被修改了,看看是否可以将提炼的代码变为一个查询,将结果给相关变量

如果这样不好做,或者被修改的变量不止一个,拷贝的方式可能就不适用了,这个时候可能还需要用到(分解临时变量/以查询替换变量)等手段了

⑥ 将被提炼代码段中需要被读取的局部变量,当参数传给目标函数

⑦ 处理结束后检查测试之,便结束!

好了,我们再来几个例子

无局部变量

 1 var log = function (msg) { console.log(msg); };
 2 var printOwing = function (amount) {
 3     //var productList = [];//这个数据你懂的
 4     var outstanding = 0;
 5     log('*****************');
 6     log('****Cunstomer Owes*****');
 7     log('*****************');
 8 
 9     for (var k in productList) {
10         outstanding += productList[k].getAmount();
11     }
12 
13     log('name:' + _name);
14     log('amount:' + outstanding);
15 };
这个重构比较简单

 1 var printOwing = function (amount) {
 2     //var productList = [];//这个数据你懂的
 3     var outstanding = 0;
 4     printBanner();
 5 
 6     for (var k in productList) {
 7         outstanding += productList[k].getAmount();
 8     }
 9 
10     log('name:' + _name);
11     log('amount:' + outstanding);
12 };
13 
14 var printBanner = function () {
15     log('*****************');
16     log('****Cunstomer Owes*****');
17     log('*****************');
18 };
但是没有局部变量只是一个传说,比如上处最后log的内容,于是来一个(简单的)

 

 1 var printOwing = function (amount) {
 2     //var productList = [];//这个数据你懂的
 3     var outstanding = 0;
 4     printBanner();
 5 
 6     for (var k in productList) {
 7         outstanding += productList[k].getAmount();
 8     }
 9 
10     printDetails(outstanding);
11 };
12 
13 var printBanner = function () {
14     log('*****************');
15     log('****Cunstomer Owes*****');
16     log('*****************');
17 };
18 
19 var printDetails = function (outstanding) {
20     log('name:' + _name);
21     log('amount:' + outstanding);
22 }
PS:此处的_name,在js里面应该是this._name

这个也相对比较简单,如果局部变量是个对象,而被提炼代码调用了会对该对象造成修改的函数,也可以这样做,只不过需要将这个对象作为参数传递给目标函数,只有在被提炼函数会对变量赋值时,有所不同,下面我们就会看到这个情况。

局部变量赋值

这个情况较复杂,这里我们看看临时变量被修改的两种情况,

比较简单的情况是这个变量只在被提炼代码段中使用,这样源代码中的这个变量就可以被消除,

另一种情况就是源代码中改了,提炼处代码也改了, 这个时候如果是之前改的就不用管了,之后会发生变化需要返回这个值。

这里我们将上述代码计算的代码提炼出来:

 1 var log = function (msg) { console.log(msg); };
 2 var printOwing = function (amount) {
 3     //var productList = [];//这个数据你懂的
 4     printBanner();
 5     printDetails(getOutStanding());
 6 };
 7 
 8 var printBanner = function () {
 9     log('*****************');
10     log('****Cunstomer Owes*****');
11     log('*****************');
12 };
13 
14 var printDetails = function (outstanding) {
15     log('name:' + _name);
16     log('amount:' + outstanding);
17 };
18 
19 var getOutStanding = function () {
20     var result = 0;
21     for (var k in productList) {
22         result += productList[k].getAmount();
23     }
24     return result;
25 };
这个例子中outstanding变量只是单纯被初始化一个明确的值,但如果其他地方做过处理,就必须作为参数传入

 1 var log = function (msg) { console.log(msg); };
 2 var printOwing = function (amount) {
 3     //var productList = [];//这个数据你懂的
 4     var outstanding = amount * 2;
 5     outstanding = getOutStanding(outstanding)
 6     printBanner();
 7     printDetails(getOutStanding());
 8 };
 9 
10 var printBanner = function () {
11     log('*****************');
12     log('****Cunstomer Owes*****');
13     log('*****************');
14 };
15 
16 var printDetails = function (outstanding) {
17     log('name:' + _name);
18     log('amount:' + outstanding);
19 };
20 
21 var getOutStanding = function (result) {
22     result = result || 0;//注意这种写法如果result为0可能导致我们程序BUG,所以数字要注意
23     for (var k in productList) {
24         result += productList[k].getAmount();
25     }
26     return result;
27 };
如果其中改变的变量不止一个,就返回对象变量吧,这个东西就暂时说到这里了,后面看实例吧。

内联函数

该方法用于消除函数,先来个代码看看

 1 var getRating = function () {
 2     return moreThanFive() ? 2 : 1;
 3 };
 4 
 5 var moreThanFive = function () {
 6     return num > 5;
 7 }
 8 
 9 var getRating = function () {
10     return num > 5 ? 2 : 1;
11 };
本来我们常以简单的函数表现动作意图,这样会使代码更为清晰,但有时候会遇到某些函数,内部代码很简单,这种情况就应该去掉这个函数

PS:这个界限不是很好把握,另一种情况是手上有一群组织不合理的函数,我们可以将它组织到一个大函数中,再从新提炼成小函数,这种情况更多见。

如果我们使用了太多中间层,使得系统所有的函数都是对另一个函数的委托,这个时候,函数会让我们晕头转向,这个时候可以去掉中间层

如何做?

① 检查函数,确定其不具有多态(如果有继承关系就不要搞他了)

② 找出函数所有调用点

③ 复制为函数本体

④ 检查,删除函数本身

内联函数比较复杂,递归调用,多返回点,

内联临时变量

你有一个临时变量,只被一个简单的表达式赋值一次,而他影响了其它重构手法,那么将所有对该变量的引用动作,替换为对它赋值的那个表达式自身

1 var basePrice = anOrder.basePrice();
2 return basePrice > 100;
4 return anOrder.basePrice() > 100
以查询取代临时变量

这个方法比较实用,程序以一个临时变量保存某一个表达式的结果时,那么将这个表达式提炼到一个独立的函数中

将这个临时变量的变量的所有引用点替换为新函数的调用,这样的话,新函数就可以被其它函数使用了

 1 function amount() {
 2     var basePrice = _quantity * _itemPrice;
 3     if (basePrice > 1000) return basePrice * 0.95;
 4     else return basePrice * 0.98;
 5 }
 6 
 7 
 8 function amount() {
 9     if (basePrice() > 1000) return basePrice() * 0.95;
10     else return basePrice() * 0.98;
11 }
12 
13 function basePrice() {
14     return _quantity * _itemPrice;
15 }
这样做的好处是,消除临时变量,因为临时变量是暂时的,只能存在所属函数,所以为了能够访问到变量,有可能我们会写出更长的函数,

如果把临时变量替换为一个查询,那么同一个类中的所有函数都可以获得这个数据,类的结构会更加清晰

查询替换变量一般会在提炼函数时候用到,该方法要用好还是不易的

怎么做?

① 找出只被赋值一次的临时变量(多次赋值需要分解临时变量了),注意这在js中可能不易

② 提炼临时变量等号右边到独立函数

③ 测试

我们常常使用临时变量保存循环中的信息,这个情况下就把整个循环提炼出来,

PS:这个时候我们可能会关心性能问题,据说这个性能不会对我们的程序有多大的影响

 

 1 function getPrice() {
 2     var basePrice = _quantity * _itemPrice;
 3     var discountFactor;
 4     if (basePrice > 1000)
 5         discountFactor = 0.95;
 6     else
 7         discountFactor = 0.98;
 8 }
 9 此时我们想替换两个临时变量
10 function getPrice() {
11     return basePrice() * discountFactor();
12 }
13 
14 function basePrice() {
15     return _quantity * _itemPrice;
16 }
17 
18 function discountFactor() {
19     return basePrice() > 1000 ? 0.95 : 0.98
20 }
引入解释性变量

如果我有一个复杂表达式,将该表达式(或者一部分)的结果放进一个临时变量,将此临时变量名用来解释表达式用途

 1 if(platform.toLocaleUpperCase().indexOf('MAC') > -1 && location.href.toLocaleUpperCase().indexOf('IE') > -1 && ......){
 2 //do someting
 3 }
 4 
 5 这种很长的条件判断很难读,这个时候临时变量反而能帮助你阅读
 6 var isMac = platform.toLocaleUpperCase().indexOf('MAC') > -1;
 7 var isIE = location.href.toLocaleUpperCase().indexOf('IE') > -1;//这个代码有问题,不必关注
 8 
 9 if(isMac && isIE && ......){
10 //do someting
11 }
但有个问题是,原作者并不推荐增加临时变量,所以,一般我们就会提炼为函数了,这样也增加重用性

分解临时变量

我们的程序有某个临时变量被赋值超过一次,他既不是循环变量又不被用于收集计算结果,那么针对每次赋值新建一个对应的临时变量

1 var temp = 2 * (_height + _width);
2 temp = _height * _width;
4 var perimeter = 2 * (_height + _width);
5 var area = _height * _width;
这样做的好处,其实就是为了避免一个变量被无意义多次使用

除了循环变量或者用于收集结果的临时变量应该被多次使用,还有一些临时变量用于保存冗长的结果会被稍后使用

如果一个变量被赋值超过一次,那么他就担任了过多的职责了 其实这样做的好处,是为了我们方便提炼函数,或者以查询替换变量的操作

 1 function getDistanceTravelled(time) {
 2     var result;
 3     var acc = _primaryForce / _mass;
 4     var primaryTime = Math.min(time, _delay);
 5 
 6     result = 0.5 * acc * primaryTime * primaryTime;
 7     var secondaryTime = time - _delay;
 8 
 9     if (secondaryTime > 0) {
10         var primaryVel = acc * _delay;
11         acc = (_primaryForce + _secondaryForce) / _mass;
12         result += primaryVel * secondaryTime + 0.5 * acc * secondaryTime * secondaryTime;
13     }
14     return result;
15 }
按照作者的话来说,这真是丑陋的代码啊,我反正抄都抄了很久



这个是一个物理中的一个神马公式我给忘了

见图,acc被两次赋值,第一次是为了保存第一次力造成的初始速度,

第二次保存两个力共同作用造成的加速度,这里我们使用final替换第二次的结果

 1 function getDistanceTravelled(time) {
 2     var result;
 3     var acc = _primaryForce / _mass;
 4     var primaryTime = Math.min(time, _delay);
 5 
 6     result = 0.5 * acc * primaryTime * primaryTime;
 7     var secondaryTime = time - _delay;
 8 
 9     if (secondaryTime > 0) {
10         var primaryVel = acc * _delay;
11         var final = (_primaryForce + _secondaryForce) / _mass;
12         result += primaryVel * secondaryTime + 0.5 * final * secondaryTime * secondaryTime;
13     }
14     return result;
15 }
然后我们再使用下其它手段试试: ①提炼函数,②以查询取代变量

PS:我知道了力/质量=重力加速度

function getDistanceTravelled(time) {
    var result = 0.5 * accelerationOfGravity(_primaryForce, _mass) 
* primaryTime(time, _delay);
    if (secondaryTime() > 0) {
        result += primaryVel(_primaryForce, _mass) * secondaryTime() 
+ 0.5 * final(_primaryForce, _secondaryForce, _mass) * secondaryTime() * secondaryTime();
    }
    return result;
}

function accelerationOfGravity(force, mass) {
    return force / mass;
}
function primaryTime(time, _delay) {
    return Math.min(time, _delay) * Math.min(time, _delay);
}
function secondaryTime() {
    return time - _delay;
}
function primaryVel(_primaryForce, _mass) {
    return accelerationOfGravity(_primaryForce, _mass) * _delay;
}
function final(_primaryForce, _secondaryForce, _mass) {
    return (_primaryForce + _secondaryForce) / _mass;
}
下面是我完成不理解程序情况下胡乱意淫改的,不必在意

移除参数赋值

代码对一个参数赋值,那么以一个临时变量取代该参数位置(这对js不知道好使不)

1 function discount(inputVal, quantity, yearToDate) {
2     if (inputVal > 50) inputVal -= 2;
3 }
4 //修改后
5 function discount(inputVal, quantity, yearToDate) {
6     var result = inputVal;
7     if (inputVal > 50) result -= 2;
8 }
这样做的目的主要为了消除按值传递与按引用传递带来的问题,这里我们来深入纠结一番

 

 1 function discount(inputVal, quantity, yearToDate) {
 2     if (inputVal > 50) inputVal -= 2;
 3 }
 4 //修改后
 5 function discount(inputVal, quantity, yearToDate) {
 6     var result = inputVal;
 7     if (inputVal > 50) result -= 2;
 8 }
 9 
10 这样做的目的主要为了消除按值传递与按引用传递带来的问题,这里我们来深入纠结一番
11 var value = 1;
12 function demoVal(p) {
13     p++;
14 }
15 console.log(value);
16 demoVal(value);
17 console.log(value);
18 
19 
20 var obj = {
21 name: 'yexiaochai',
22 age: 20
23 };
24 function demoRefer(obj) {
25     obj.age++;
26 }
27 console.log(obj);
28 demoRefer(obj);
29 console.log(obj);


所以你懂的,函数内部操作,有时无意就会改变传入参数

以函数对象取代函数

我们有一个大型函数,其中对局部变量的使用使你无法采用提炼函数方法,将这个函数放进一个单独对象中,如此一来局部变量就成了对象内的字段

然后你可以在同一个对象中将这个大型函数分解为多个小型函数 函数分解难度较高,将变量替换为查询可以减小他的难度,如果也不行的话,就将函数对象化吧

这里来一个相当简化的代码:

 1 function gamma(inputVal, quantity, yearToDate) {
 2     var v1 = (inputVal * quantity) + delta();
 3     var v2 = (inputVal * yearToDate) + 100;
 4 
 5     if (yearToDate - v1 > 100) v2 -= 20;
 6     var v3 = v2 * 7;
 7 
 8     //......
 9     return v3 - 2 * v1;
10 }
为了说明这个问题,作者写了一段莫名其妙的代码,我一看,确实莫名其妙......

 1 function delta() {return 10; }
 2 function gamma(inputVal, quantity, yearToDate) {
 3     var v1 = (inputVal * quantity) + delta();
 4     var v2 = (inputVal * yearToDate) + 100;
 5 
 6     if (yearToDate - v1 > 100) v2 -= 20;
 7     var v3 = v2 * 7;
 8 
 9     //......
10     return v3 - 2 * v1;
11 }
12 
13 var Gamma = function (opts) {
14     this.inputVal = opts.inputVal;
15     this.quantity = opts.quantity;
16     this.yearToDate = opts.yearToDate;
17     this.v1;
18     this.v2;
19     this.v3;
20 };
21 Gamma.prototype = {
22     computer: function () {
23         this.alter1();
24         this.alter2();
25         if (this.yearToDate - this.v1 > 100) this.v2 -= 20;
26         this.v3 = this.v2 * 7;
27 
28         //......
29         return this.v3 - 2 * this.v1;
30     },
31     alter1: function () {
32         this.v1 = (this.inputVal * this.quantity) + delta();
33     },
34     alter2: function () {
35         this.v2 = (this.inputVal * this.yearToDate) + 100;
36     }
37     //....
38 
39 };
40 
41 //demo
42 console.log(gamma(5, 6, 7));//865
43 
44 var d = new Gamma({
45 inputVal: 5, 
46 quantity: 6, 
47 yearToDate: 7});
48 console.log(d.computer());//865
替换算法

替换算法其实是最难的,在你不熟悉代码业务与逻辑时,你去改的话,兄弟你们就坑吧!!! 来个简单的例子结束今天的任务

 1 function find(data) {
 2     if (data == 1) return '周日';
 3     if (data == 2) return '周一';
 4 
 5     //...
 6     return null;
 7 }
 8 
 9 function find(data) {
10             

11     return ['周日', '周一', /*......*/][data];
12 }
 

OK,进入任务结束,高高兴兴打游戏了!


本文转自叶小钗博客园博客,原文链接http://www.cnblogs.com/yexiaochai/p/3371121.html如需转载请自行联系原作者
相关文章
|
7月前
|
前端开发 JavaScript 测试技术
修改代码的艺术——如何高效开发、维护和重构复杂的现有系统
这篇文章回忆了作者在高三时期通过努力进入班级前列的故事,并引申到软件开发领域。作者指出,开发工作往往被认为困难重重,但实际上,通过良好的方法、设计和工具,可以提高开发效率和享受编程带来的成就感。文章以最近完成的一个复杂核心需求为例,详细介绍了如何分析、设计和实现这个需求,包括采用领域驱动设计(DDD)理念,数据库字段变更,代码实现,自动化单元测试,重构和代码维护的重要性。最后,作者推荐了几本关于软件开发的经典书籍,并鼓励开发者不断提升自己,以更好地应对挑战。
|
6月前
|
存储 设计模式 移动开发
编程怎样如何组织数据和代码
编程怎样如何组织数据和代码
|
设计模式
重构·改善既有代码的设计.04之重构手法(下)完结
重构改善既有代码的设计完结篇,汇总了全部的重构手法。看看哪些手法对你的项目能有所帮助…
7422 2
重构·改善既有代码的设计.04之重构手法(下)完结
|
设计模式 程序员 开发者
重构·改善既有代码的设计.01之入门基础
近期在看Martin Fowler著作的《重构.改善既有代码的设计》这本书,这是一本经典著作。书本封面誉为软件开发的不朽经典。书中从一个简单的案例揭示了重构的过程以及最佳实践。同时给出了重构原则,何时重构,以及重构的手法。用来改善既有代码的设计,提升软件的可维护性。
640 1
重构·改善既有代码的设计.01之入门基础
|
数据处理
《重构2》第六章-重构基础
《重构2》第六章-重构基础
310 0
|
算法
第七章 多用模板专注设计(上)
第七章 多用模板专注设计
104 0
第七章 多用模板专注设计(上)
重构改善既有代码的设计---笔记
重构改善既有代码的设计---笔记
225 0
|
程序员
《重构:改善既有代码的设计》-学习笔记二(+实战解析)
《重构:改善既有代码的设计》-学习笔记二(+实战解析)
583 0
《重构:改善既有代码的设计》-学习笔记二(+实战解析)
|
设计模式 Java 程序员
《重构:改善既有代码的设计》-学习笔记一(+实战解析)
《重构:改善既有代码的设计》-学习笔记一(+实战解析)
211 0
《重构:改善既有代码的设计》-学习笔记一(+实战解析)
下一篇
DataWorks