【重构笔记06】简化函数调用
前言
在对象技术中,最重要的概念莫过于“接口interface”,容易被理解和使用的接口,是开发良好面向对象软件的关键,本章将介绍几个被接口变得更简洁易用的重构手法。
改名
① 最简单也是最重要的一件事情就是修改函数名,名称是程序作者与阅读者交流的关键工具
PS:如果说HTML是构造世界的话,里面最恐怖的是就是给一山一水建立class名,你会发现是一个巨大的工程!
② 函数参数在接口中扮演着十分重要的角色,增减参数都是常见的重构手法,刚接触面向对象的程序员往往使用很长的参数列,但是面向对象的参数可以保持参数简短
如果来自同一对象的多个值被当做参数传递,可以使用保持对象完整避免传递参数,而采用引入参数对象......
读写分离
我们程序过程中可以明确的将“修改对象状态”的修改函数与“查询对象状态”的查询函数分开设计
隐藏细节
良好的接口只向用户提供必需展现的东西,如果一个接口暴露了过多细节,我们就需要将细节隐藏,从而改变接口质量
首先,所有数据都应该隐藏,然后所有私有方法也该隐藏,就算是javascript这种不可隐藏的对象,也可以使用_Method方式装做隐藏
我们做的时候可以先方便程序编写而最后隐藏
工厂函数取代构造函数
构造函数是比较麻烦的一个东西,他强迫我们必须知道要创建对象属于哪一个类,而且往往我们并不想知道,这个时候就可以用工厂函数取代构造函数了
所以我们一起来看看具体怎么做吧!
函数改名
如果函数名称未能解释函数的用途,那么修改函数名称吧!!!
将复杂的处理的处理过程分解成小函数是一种好的习惯,这样代码会变得清晰
但是如果做的不好,就会让我们费尽周折却弄不清小函数的用途,避免这个问题的关键就是函数取名,而由于英语水平我们往往都做的很糟糕,所以,必要的注释必不可少!!!
PS:其实我注释写的比较糟糕,一个比较好的格式如下:
/** * 取model数据 * @param {Function} onComplete 取完的回调函 * 传入的第一个参数为model的数第二个数据为元数据,元数据为ajax下发时的ServerCode,Message等数 * @param {Function} onError 发生错误时的回调 * @param {Boolean} ajaxOnly 可选,默认为false当为true时只使用ajax调取数据 * @param {Boolean} scope 可选,设定回调函数this指向的对象 * @param {Function} onAbort 可选,但取消时会调用的函数 */ function(......){}
PS:这个注释可以放到类上面,可以放到方法上面,并且标注作者,让人可以找到你......
取名难
经常出现的情况是我们第一次并不能取一个好名字,甚至会拼写错误!!!这个时候往往就睁一只眼闭一只眼过去了
当心一点,这是一个坑!如果一个对外接口拼写错误/如果一个对外接口取名不好一旦有人用了,你就哭吧!!!
要成为高手,取名就是第一步,就算准高手或者高手很多时候取名都是坑,所以好好取名吧!
怎么干?
① 检查函数名是否被超类实现,如果被实现就需要声明新函数,将旧函数代码复制过去
② 修改旧函数。将调用转发给新函数
③ 找出旧函数引用点,修改之让他们改用新函数,然后删除旧函数
范例
1 var Person = base.Class({ 2 _propertys_: function () { 3 this.officeAreaCode = ''; 4 this.officeAreaNumber = ''; 5 }, 6 getTel: function () { 7 return '(' + this.officeAreaCode + ')' + this.officeAreaNumber; 8 } 9 });
现在我们将函数名改得更合理一点
1 var Person = base.Class({ 2 _propertys_: function () { 3 this.officeAreaCode = ''; 4 this.officeAreaNumber = ''; 5 }, 6 getTel: function () { 7 return '(' + this.officeAreaCode + ')' + this.officeAreaNumber; 8 }, 9 getOfficeNumber: function () { 10 return this.getTel(); 11 } 12 });
这是,旧函数其实可以删除也可以留着,全看当时的使用情况了
添加/移除参数
添加参数
如果某个函数需要从调用端得到更多信息,为此函数添加一个对象参数,让该对象带进函数所需信息
添加参数是一个常使用的重构手段,我们修改一个函数时,如果需要一些过去没有的信息,这时就需要给该函数添加一个参数了
其实不使用这个方法我们依然有其它可选方案,因为参数过长的感觉不是很好!
PS:这个规律对js来说不太适用,这个我们就不深入了解了......
移除参数
有增便有减,而且适当的移动参数顺序也是不错的
将查询和修改函数分离
如果某个函数既返回对象值又修改对象状态,那么建立两个函数,一个负责查询一个负责修改
如果某个函数只是想你提供一个值,没有其它副作用,那么就任意调用这个函数
任何有返回值的函数,都不应该有明显的副作用,如果我们遇到一个既有返回值,又有副作用的函数,就应该分离了
怎么干?
① 新建一个查询函数,令他返回值与原函数相同,观察原函数,看他返回什么东西,如果是临时变量需要找出临时变量位置
② 修改原函数,令他调用查询函数,并返回获得结果
原函数的每个return都应该像这样:
return newQuery()
而不应该返回其它东西,如果调用者将返回值给了一个临时变量,你应该去除这个临时变量
PS:我其实在程序过程中经常借用临时变量,为了减少计算,看来这个习惯要改了
③ 将调用原函数的代码改为调用查询函数,然后在调用查询函数的那一行之前加上对原函数的调用,修改编译
范例
有这样一个函数,一旦有人入侵安全系统,他会告诉我们侵略者的名字,并发送警报,如果入侵者不止一个,也只发一次警报
1 function findInSet(names, arr) { 2 if (typeof names != 'object') names = [names]; 3 var ret = []; 4 for (var k in names) { 5 for (var i = 0, len = arr.length; i < len; i++) { 6 if (arr[i] == names[k]) ret.push(names[k]); 7 } 8 } 9 return ret; 10 } 11 function sendAlert() { 12 console.log('警报'); 13 } 14 function foundMiscreant(people) { 15 for (var i = 0, len = people.length; i < len; i++) { 16 if (findInSet(['Don', 'John'], people).length > 0) { 17 (typeof sendAlert == 'function') && sendAlert(); 18 return findInSet(['Don', 'John'], people); 19 } 20 } 21 return ''; 22 } 23 //调用者 24 function checkSecurity(people) { 25 var found = foundMiscreant(people); 26 (typeof someMethod == 'function') && someMethod(found); 27 } 28 console.log(checkSecurity(['1', '2', 'Don']));
其实这里的代码我做了一次优化,原来作者的意思是将发警报与查找的逻辑分离,我们这样干:
1 function findInSet(names, arr) { 2 if (typeof names != 'object') names = [names]; 3 var ret = []; 4 for (var k in names) { 5 for (var i = 0, len = arr.length; i < len; i++) { 6 if (arr[i] == names[k]) ret.push(names[k]); 7 } 8 } 9 return ret; 10 } 11 function sendAlert() { 12 console.log('警报'); 13 } 14 function _sendAlert(people) { 15 for (var i = 0, len = people.length; i < len; i++) { 16 if (findInSet(['Don', 'John'], people).length > 0) { 17 return findInSet(['Don', 'John'], people); 18 } 19 } 20 return []; 21 } 22 23 //调用者 24 function checkSecurity(people) { 25 var found = findInSet(['Don', 'John'], people); 26 (typeof _sendAlert == 'function') && _sendAlert(found); 27 } 28 console.log(checkSecurity(['1', '2', 'Don']));
令函数携带参数
若干函数做了类似的工作,但函数本体却包含了不同的值,那么建立一个函数,以参数表达那些不同的值
这种情况经常发生,明明干的事情差不多的函数,却因为几个值致使行为略有不同,这种情况让人恨啊!
这个时候可以将分离的函数统一,通过参数来处理变化情况,这样可以去除重复代码,并提高灵活性,所以干掉他们吧
范例
function tenPercent() { return this.salary * 1.1; } function fivePercent() { return this.salary * 1.05; }
这个比较简单
function raise(factor) { return this.salary * factor; }
我们下面来个较复杂的
1 var lastUsage = function () { 2 return 10; 3 }; 4 function baseCharge() { 5 var result = Math.min(lastUsage(), 100) * 0.3 6 if (lastUsage() > 100) { 7 result += (Math.min(lastUsage(), 100) - 100) * 0.05; 8 } 9 if (lastUsage() > 200) { 10 result += (lastUsage() - 200) * 0.07; 11 } 12 return result; 13 }
科学计算的情况下,这个代码很容易出现,我们将相同的分离出来
1 function baseCharge() { 2 var result = usageInRange(0, 100) * 0.3 3 result += usageInRange(100, 200) * 0.05; 4 result += usageInRange(200, 10000000000) * 0.07; 5 return result; 6 } 7 function usageInRange(start, end) { 8 if (lastUsage() > start) return Math.min(lastUsage(), end) - start; 9 return 0; 10 }
他这个优化挺巧妙的
以明确函数取代参数
我们有一个函数,其中完全取决于参数值而采取不同的行为,那么针对该参数的每一个可能的值,建立一个独立函数
这个跟上一个方法恰好相反,如果某个参数有多个可能的值,而函数内又以条件表达式检查这些参数值,并根据不同的参数有不同的行为,那么就该使用此重构
调用者原本必须给予适当的参数,再决定做和反应,现在提供不同的函数就可以避免条件表达式了
范例
1 function setValue(name, value) { 2 if (name == 'height') { 3 this.height = value; 4 return; 5 } 6 if (name == 'width') { 7 this.width = value; 8 return; 9 } 10 }
这个优化较简单,其实就是简单的设值函数,清晰表达含义
1 function setWidth(arg) { 2 this.width = arg; 3 } 4 function setHeight(arg) { 5 this.height = arg; 6 }
保持对象完整
你从某个对象中取出若干值,将他们作为某一次函数调用的参数,那么改为传递整个对象
1 //一天的最高最低温度 2 function dayTempRange() { 3 return { 4 low: 0, 5 high: 40 6 }; 7 } 8 9 var low = dayTempRange().low; 10 var high = dayTempRange().high; 11 var withPlan = someMethod(low, high); 12 13 //重构后 14 var withPlan = someMethod(dayTempRange());
有时候我们会将来自同一对象的数据分为若干项传递给一个函数,这样会有一个问题:万一将来被调用函数需要新的数据项怎么办?
我是不是还需要修改对此函数的所有调用???想来就觉得恐怖,整个对象传入可以避免这个问题
PS:在js中我们都是整个传递的......
但,这样做也不是完全没有问题的,原来依赖数值的函数现在需要依赖对象了,这样会带来依赖结果的恶化
范例
我们有一个Room的类,他会记录房间一天最高与最低温度,然后将预计温度与实际温度做对比,再告诉用户当天是否符合要求
1 var Room = base.Class({ 2 withinPlan: function (plan) { 3 var low = this.dayTempRange().low; 4 var high = this.dayTempRange().high; 5 var withPlan = plan.withinPlan(low, high); 6 }, 7 daysTempRange: function () { 8 } 9 }); 10 11 var HeatingPlan = base.Class({ 12 _propertys_: function () { 13 this.range = {}; 14 }, 15 withinPlan: function (low, high) { 16 return (low >= this.range.getLow()) && high <= this.range.getHigh(); 17 } 18 });
我们并不需要将TempRange对象的信息拆开单独传递,只需要将整个对象传递给withinPlan即可
withinPlan: function (roomRange) { return (roomRange.getLow() >= this.range.getLow()) && (roomRange.getHigh() <= this.range.getHigh()); }
以函数取代参数
对象调用某个函数,并将所得结果作为参数,传递给另一个函数,而接受该参数的函数本身也能调用那个函数,那么去除该参数项,直接调用即可
1 var basePrice = quantity * price; 2 var discount = getDiscount(); 3 var finalPrice = discountedPrice(basePrice, discount);
调整为这个样子,在函数内部自己调用函数
var finalPrice = discountedPrice(basePrice);
如果函数可以通过其他途径获得参数值,那么他就不应该通过参数取得该值,过长的参数列表会增加程序阅读者的难度,因此我们应该控制参数长度
范例
我们有一个用于计算订单折扣的价格,我们来看看他的实现
1 function getPrice() { 2 var basePrice = num * price; 3 var discountLevel; 4 if (num > 100) discountLevel = 2; 5 else discountLevel = 1; 6 var finalPrice = discountedPrice(basePrice, discountLevel); 7 } 8 function discountedPrice(basePrice, discountLevel) { 9 if (discountLevel == 2) return basePrice * 0.1; 10 return basePrice * 0.05; 11 }
首先,我们将计算折扣等级的代码提炼出来/顺便将获得basePrice提炼了吧
1 function getPrice() { 2 var finalPrice = discountedPrice(); 3 } 4 function discountedPrice() { 5 if (getDiscountLevel() == 2) return getBasePrice() * 0.1; 6 return getBasePrice() * 0.05; 7 } 8 function getBasePrice() { 9 return num * price; 10 } 11 function getDiscountLevel() { 12 if (num > 100) return 2; 13 else return 1; 14 }
这样干完后,我们每个函数的长度都会很小,看上去美极了。。。。。。(但是阅读时在不同函数跳来跳去也容易晕,所以函数的位置排版也是学问)
引入参数对象
某些参数总是很自然的出现,以一个对象取代这些参数
我们常常会看到一组特定的参数被一起传递,可能有好几个函数都使用这一组参数,这些参数可能属于同一个类,也可能属于不同的类
这样的参数就是“data clumps(数据泥团)”,我们可以运用一个对象包装所有这些数据,再以对象取代之
范例
我们有一个账面和帐项的程序,表示账项的Entry实际只是简单的数据容器
1 var Entry = base.Class({ 2 _propertys_: function () { 3 4 }, 5 init: function (value, chargeDate) { 6 this.chargeDate = chargeDate; 7 this.value = value; 8 }, 9 getValue: function () { 10 return this.value; 11 } 12 });
我们的关注焦点是用以表示“账目”的Account,他保存了一组Entry对象,并有一个函数用来计算两个日期间账目总和
1 var Account = base.Class({ 2 _propertys_: function () { 3 }, 4 init: function (entries) { 5 this.entries = entries || []; //一组账目项 6 }, 7 getFlowBetween: function (start, end) { 8 var result = 0; 9 for (var k in this.entries) { 10 if (this.entries[k].getDate() >= start || this.entries[k].getDate() <= end) { 11 result += this.entries[k].getValue(); 12 } 13 } 14 return result; 15 } 16 });
我们的代码中会出现表示日期的范围或者数值的范围,这个时候,我们可以使用范围对象取代之,我们首先需要声明一个简单的数据容器
1 var DateRange = base.Class({ 2 init: function (start, end) { 3 this.start = start; 4 this.end = end; 5 }, 6 getStart: function () { 7 return this.start; 8 }, 9 getEnd: function () { 10 return this.end; 11 } 12 });
接下来,我们就可以将DateRange对象加到函数中了
1 var Account = base.Class({ 2 _propertys_: function () { 3 }, 4 init: function (entries) { 5 this.entries = entries || []; //一组账目项 6 }, 7 getFlowBetween: function (dateRange) { 8 var result = 0; 9 for (var k in this.entries) { 10 if (this.entries[k].getDate() >= dateRange.getStart() || this.entries[k].getDate() <= dateRange.getEnd()) { 11 result += this.entries[k].getValue(); 12 } 13 } 14 return result; 15 } 16 });
既然已经这样了,我们不妨将getFlowBetween中计算的逻辑搬出来吧,最后形成了这样的代码:
1 var Entry = base.Class({ 2 _propertys_: function () { 3 }, 4 init: function (value, chargeDate) { 5 this.chargeDate = chargeDate; 6 this.value = value; 7 }, 8 getValue: function () { 9 return this.value; 10 }, 11 getDate: function () { 12 return this.chargeDate; 13 } 14 }); 15 var DateRange = base.Class({ 16 init: function (start, end) { 17 this.start = start; 18 this.end = end; 19 }, 20 getStart: function () { 21 return this.start; 22 }, 23 getEnd: function () { 24 return this.end; 25 }, 26 isInclude: function (arg) { 27 if (arg >= this.getStart() || arg <= this.getEnd()) { 28 return true; 29 } 30 return false; 31 } 32 }); 33 var Account = base.Class({ 34 _propertys_: function () { 35 }, 36 init: function (entries) { 37 this.entries = entries || []; //一组账目项 38 }, 39 getFlowBetween: function (dateRange) { 40 var result = 0; 41 for (var k in this.entries) { 42 if (dateRange.isInclude(this.entries[k].getDate())) result += this.entries[k].getValue(); 43 } 44 return result; 45 } 46 });
移除设值函数
类中的某个字段应该在对象创建时被赋值,然后就不再改变,那么去掉字段的所有设值函数
这个很好理解,如果我们为某个字段用到了设值函数,就意味着该字段可写,如果不希望在创建后辈修改就不要搞设值函数了,比如上例的开始于结束时间,一旦初始化就不允许修改
这个就不写例子了
隐藏函数
有一个函数,从来没有实例用到他,那么我们需要将他隐藏,在js中没有private的概念,我们可以使用以下方法隐藏
1 (function () { 2 //方法一 3 var privateMethed = function (){}; 4 var _Class= base.Class({ 5 //方法二 6 _privateMethed: function () { 7 8 } 9 }); 10 })();
上面的方法都是可以的,如果是与类无关的工具方法,可以放到类定义外面,如果会用到this指向的话,就加个标志在前面吧,我们看不见......
PS:这项重构难度其实较高,我们在做一个程序或者功能时,开始就必须想到要对外提供哪些接口,需要提供的接口才暴露,否则不可暴露
以工厂函数取代构造函数
我们有时希望在创建对象时候不仅是简单的构造动作,那么就是要工厂函数吧
1 var Employee = base.Class({ 2 _propertys_: function () { 3 }, 4 init: function (arg) { 5 this.type = arg; 6 } 7 }); 8 //改为这样 9 function create(arg) { 10 return new Employee(arg); 11 }
使用该方法最主要的原因是在派生子类过程中以工厂函数取代类型码,我们可能常常需要根据类型码选择创建响应对象
封装向下转型
某个函数返回对象,需要由函数调用者执行向下转型将向下转型动作移到函数中
PS,转型对JS来说就是渣,所以我根本不关注了......
结语
本章暂时到这里,至此我们重构的学习终于过了一大半了,争取最近结束第一次学习,尼玛我发现现在学东西变慢了......