zl程序教程

您现在的位置是:首页 >  Python

当前栏目

重构,有品位的代码之重构API

2023-03-07 09:49:02 时间

本文转载自微信公众号「前端万有引力」,作者 一川 。转载本文请联系前端万有引力公众号。

写在前面

伙伴们,最近事情有点多、空余时间都花在学习新知识、新技术以及巩固基础上了,在实践开发越来越觉得自己的技术和能力有限,认识到了自己的短板和不足。后面我会把自己所学所看,以及在项目实践中对方法进行总结,分享给各位伙伴们共同学习、批评指正。

今天就继续分享《重构,有品味的代码》系列第八篇文章,As you know,模块和函数组成了软件的钢筋水泥,而api就是整个软件建筑的栋和梁。显而易见,在对软件开发有了深层次的理解,我们会发现如何改进api将更新数据的函数和读取数据的函数进行分割。让每个函数都做自己的本分,衔接它们之间的事情交给模块去调用。

重构API

常见的重构API的方法有:

  • 将查询函数和修改函数分离
  • 函数参数化
  • 移除标记参数
  • 保证完整性
  • 以查询取代参数
  • 以参数取代查询
  • 移除设值函数
  • 以工厂函数取代构造函数
  • 以命令取代函数
  • 以函数取代命令

1. 将查询函数和修改函数分离

如果函数只是作为取值函数,没有其他多余的实现功能,那么这个函数是很单纯的、很有价值的东西。因为可以任意调用此函数,可以在整个项目的任意角落使用,无需担心有其它多余的累赘。记住:任何有返回值的函数,不应该有其它多余的功能,即命令和查询分开。

通常做法是:拷贝整个函数将其作为一个查询来命名,在新建的此查询函数中移除所有有附加功能的语句,并对其进行检查原函数的所有调用处。如果调用处使用了该函数的返回值,就将其改为调用新建的查询函数,并在下面立刻进行一次调用,且从原函数中移除返回值。

举个栗子

  1. //原始写法 
  2. const setOk = ()=>{...} 
  3. const selectPeopleFun = (people)=>{ 
  4.   for(let p in people){ 
  5.     if(p === "yichuan"){ 
  6.       setOk(); 
  7.       return "good"
  8.     } 
  9.     if(p === "onechuan"){ 
  10.       setOk(); 
  11.       return "ok"
  12.     } 
  13.     return ""
  14.   } 
  15.  
  16. //重构写法 
  17. const setOk = ()=>{...} 
  18. const findNull = (people)=>{ 
  19.   for(const p of people){ 
  20.     if(p === "yichuan"){ 
  21.       setOk(); 
  22.       return
  23.     } 
  24.     if(p === "onechuan"){ 
  25.       setOk(); 
  26.       return
  27.     } 
  28.   } 
  29.   return
  30. const selectPeopleFun = (people)=>{ 
  31.   if(findNull(people) !== "") setOk(); 

2. 函数参数化

当我们发现两个函数的逻辑非常相似,只有某些字面量值不同时,可以将其进行抽取合并成一个函数,以参数的形式传入不同的值,从而消除重复的逻辑。此重构方法能够使得逻辑更加简洁、复用性强,因为每个函数都可以进行多次使用。

举个栗子

  1. //原始逻辑 
  2. function useFun(param){...} 
  3. function baseFunction(param){ 
  4.   if(param < 0) return useFun(param); 
  5.   const amount = bottomFun(param) * 0.1 + middleFun(param) * 0.2 + topFun(param) *0.3; 
  6.   return useFun(amount); 
  7.  
  8. function bottomFun(param){ 
  9.   return Math.min(param,100) 
  10. function middleFun(param){ 
  11.   return param > 100 ? Math.min(param,200) - 100 :0; 
  12. function topFun(param){ 
  13.   return param > 200 ? param - 200 : 0; 
  14.  
  15. //重构代码 
  16. function commonFun(param,bottom,top){ 
  17.   return param > bottom ? Math.min(param,top) - bottom:0; 
  18. function baseFun(param){ 
  19.   if(param<0) return useFun(0); 
  20.   const amount = commonFun(param,0,100) * 0.1 + commonFun(param,100,200) * 0.2 + commonFun(param,200,Infinity) *0.3; 
  21.   return useFun(amount); 

3. 移除标记参数

标记参数直接理解就是作为标记的参数,即通常调用者用其来只是被调用函数应该执行哪部分逻辑。但事与愿违,标记参数在实际使用过程中并没达到作为标记的作用,令人难以理解到底哪部分函数可以调用、应该如何调用。通常我们通过API查看哪部分是可调用函数,但是编辑参数却会进行隐藏函数调用中存在的差异性,在使用这些函数我们还得阅读上下文中标记参数有哪些可用的值。

要知道布尔值作为标记是多么荒唐的使用方法,因为其不能见名知意的传递信息,在函数调用时很难厘清true代表的含义,但是明确使用函数完成单独的任务,就显得清晰的多。

当然并非所有的类似参数都是标记参数,如果调用者传入的程序中不断传递的数据,那么这样的参数就不叫做标记参数。只有当调用者初入字面量值时,或者在函数内部只有参数影响了函数内部的控制流,此时作为参数就是标记参数。

移除标记参数不仅使得代码更加整洁,并且能够帮助开发工具更好的发挥作用。去掉标记参数后,代码分析工具能够更清晰体现“高级”和“普通”逻辑在使用时的区别。如果某个函数有多个标记参数,此时想要移除得花费功夫,得不偿失还不如将其保留,但是也侧面证明此函数做的太多,需要将其逻辑进行简化。

举个栗子

  1. //原始代码 
  2. function setFun(name,value){ 
  3.   if(name === "height"){ 
  4.     this._height = value; 
  5.     return
  6.   } 
  7.   if(name === "width"){ 
  8.     this._width = value; 
  9.     return
  10.   } 
  11.  
  12. //重构代码 
  13. function setHeight(value){ 
  14.   this._height = value; 
  15. function setWidth(value){ 
  16.   this._width = value; 

4. 保证完整性

当看到代码从一个记录结构中导出几个值,然后又把这几个值传递给一个函数,那么可以把整个记录传递给这个函数,在函数内部导出所需要的值。

  1. //原始代码 
  2. const low = aRoom.dayRange.low; 
  3. const high = aRoom.dayRange.high; 
  4. if(plan.goodRange(low,high)){...} 
  5. //重构代码 
  6. if(plan.goodRange(aRoom.dayRange)){...} 

5. 以查询取代参数

函数的参数列表应该总结该函数的可变性,标识出函数可能体现出行为差异的主要方式,但是参数列表又应该尽量避免冗余,因为短小精悍易理解。什么是冗余,就是倘若调用函数中传入一个值,而这个值由函数自己获取,这个本不必要的参数会增加调用者的难度,因为调用者不得不去找出此参数定义的位置。

如果想要移除得参数值只需要向另一个参数值查询即可得到,这就可以使用以查询代替参数;如果在处理的函数具有引用透明性,即在任何时候只要传入相同的参数值,该函数的行为永远一致,可以让它访问一个全局变量。

6. 以参数取代查询

在浏览函数实现时,会经常发现一些糟糕的引用关系,比如引用一些全局变量或者另一个想要移除得元素,其实可以通过将其替换成函数参数来解决,将处理引用关系的责任推卸给函数调用者。其实此重构思想是:改变代码的依赖关系,让目标函数不再依赖某个元素,将元素的值以参数形式进行传递给函数。当然,如果把所有依赖关系都变成参数,会导致参数列表冗长重复,其次倘若作用域间的共享太多,会导致函数间过度依赖。

具体做法:将执行查询操作的代码进行变量提炼,将其从函数体中分离,对现有函数体代码不再执行查询操作,而是使用上一步提炼的变量,对此部分代码使用函数提炼。使用内联变量就是把提炼出来的变量放到一个函数中,且对原先的函数使用内联函数。

  1. targetFun(plan) 
  2. const otherFun = {...} 
  3. function targetFun(plan){ 
  4.   curPlan =  otherFun.curPlan 
  5.   ... 
  6.  
  7. //重构 
  8. targetFun(plan) 
  9. function targetFun(plan,curPlan){ 
  10.   ... 

7. 移除设值函数

当为某个字段提供了设置函数,表示此字段被改变,如果不希望在对象创建之后字段被改变,就不要提供设值函数,同时声明此字段不可改变。但是呢,有些开发者喜欢通过访问函数来读取字段值,在构造函数内也是,这就会导致构造函数成为设值函数的唯一使用者,就这样你还不如直接移除设值函数呢,没有意义。

当然,对象有可能是由客户端通过脚本(通过调用构造函数,即一系列的设置函数)进行构造出来的,而不是只有一次简单的构造函数调用。在执行完创建脚本后,此新生对象的部分字段不应该再被修改,设值函数只能被允许在起初对象创建过程中被调用。其实此时也应该移除设值函数,能够更加清晰的表达意图。

  1. class User
  2.   get(){...} 
  3.   set(){...} 
  4.  
  5. //重构 
  6. class User
  7.   get(){...} 

8. 以工厂函数取代构造函数

很多面向对象语言都有构造函数用于对象的初始化,通常客户端会通过调用构造函数来新建对象。但对于普通函数而言,构造函数具有一定的局限性,通常只能返回当前所调用类的实例,就是无法根据环境或参数信息返回子类实例或代理对象。且构造函数名字是固定的,因此无法使用比默认名字更清晰的函数名,此外还需要通过特殊的操作符(关键字new)来创建实例调用。然而,工厂函数就不受限制,可以实现内部调用构造函数,也可以使用其他方式调用。

9. 以命令取代函数

函数可以是作为独立函数,也可以作为类对象中的方法,还是作为程序设计的基本构造模块。将函数封装成自己的对象成为命令对象,当然这种对象大多只服务于单一函数,获得该函数的请求并进行执行函数,就是这种对象存在的意义。

与普通函数相比,命令对象提供了更加强大的控制灵活性和更强的表达能力,除了函数调用本身,命令对象还可以作为支持附加的操作,比如撤销。可以通过命令对象提供的方法进行设置和取值操作,从而提升丰富的生命周期管理能力。

具体方法:为想要包装的函数创建一个空类,并根据该函数的名字命名类,将函数搬移到空类中,并对每个参数创建一个字段,在构造函数中添加对应的参数。

举个栗子

  1. //原始代码 
  2. function user(name,work,address){ 
  3.   let result = ""
  4.   let addressLevel =""
  5.   ...long code 
  6.  
  7. //重构代码 
  8. class User
  9.   constructor(name,work,address){ 
  10.     this._name = name
  11.     this._work = work
  12.     this._addrsss = address; 
  13.   } 
  14.   clac(){ 
  15.     this._result=""
  16.     this._addressLevel =""
  17.     ...long code 
  18.   } 

10. 以函数取代命令

命令对象为处理复杂计算提供了强大的机制,可以轻松将原本复杂的函数拆分成多个方法,彼此之间通过字段进行状态共享。拆分后的方法可以分别进行调用,开始调用之前的数据状态也可以逐步构建,但是这种强大功能是有代价的。通常我们调用函数让其完成自身的任务,当此函数不是很复杂时,命令对象显得得不偿失,还不如使用普通函数呢。

通常的,将创建并执行命令对象的代码单独提炼到独立函数中,对命令对象在执行阶段用到的函数逐一使用内联函数。使用改变函数声明,将构造函数的参数转移到执行函数。对于所有的字段在执行函数中找到引用它的地方,并将其改为使用参数,将调用构造函数和调用执行函数两步进行内联到调用函数中。

举个栗子

  1. //原始代码 
  2. class ChargeClass{ 
  3.   constructor(custom,param){ 
  4.     this._custom = custom; 
  5.     this._param = param; 
  6.   } 
  7.   clac(){ 
  8.     return this._custom.rate * this._param 
  9.   } 
  10.  
  11. //重构代码 
  12. function charge(custom,param){ 
  13.   return custom.rate * param;