本文转载自微信公众号「前端万有引力」,作者 一川 。转载本文请联系前端万有引力公众号。
写在前面
伙伴们,最近事情有点多、空余时间都花在学习新知识、新技术以及巩固基础上了,在实践开发越来越觉得自己的技术和能力有限,认识到了自己的短板和不足。后面我会把自己所学所看,以及在项目实践中对方法进行总结,分享给各位伙伴们共同学习、批评指正。
今天就继续分享《重构,有品味的代码》系列第八篇文章,As you know,模块和函数组成了软件的钢筋水泥,而api就是整个软件建筑的栋和梁。显而易见,在对软件开发有了深层次的理解,我们会发现如何改进api将更新数据的函数和读取数据的函数进行分割。让每个函数都做自己的本分,衔接它们之间的事情交给模块去调用。
重构API
常见的重构API的方法有:
- 将查询函数和修改函数分离
- 函数参数化
- 移除标记参数
- 保证完整性
- 以查询取代参数
- 以参数取代查询
- 移除设值函数
- 以工厂函数取代构造函数
- 以命令取代函数
- 以函数取代命令
1. 将查询函数和修改函数分离
如果函数只是作为取值函数,没有其他多余的实现功能,那么这个函数是很单纯的、很有价值的东西。因为可以任意调用此函数,可以在整个项目的任意角落使用,无需担心有其它多余的累赘。记住:任何有返回值的函数,不应该有其它多余的功能,即命令和查询分开。
通常做法是:拷贝整个函数将其作为一个查询来命名,在新建的此查询函数中移除所有有附加功能的语句,并对其进行检查原函数的所有调用处。如果调用处使用了该函数的返回值,就将其改为调用新建的查询函数,并在下面立刻进行一次调用,且从原函数中移除返回值。
举个栗子
- //原始写法
- const setOk = ()=>{...}
- const selectPeopleFun = (people)=>{
- for(let p in people){
- if(p === "yichuan"){
- setOk();
- return "good";
- }
- if(p === "onechuan"){
- setOk();
- return "ok";
- }
- return "";
- }
- }
- //重构写法
- const setOk = ()=>{...}
- const findNull = (people)=>{
- for(const p of people){
- if(p === "yichuan"){
- setOk();
- return;
- }
- if(p === "onechuan"){
- setOk();
- return;
- }
- }
- return;
- }
- const selectPeopleFun = (people)=>{
- if(findNull(people) !== "") setOk();
- }
2. 函数参数化
当我们发现两个函数的逻辑非常相似,只有某些字面量值不同时,可以将其进行抽取合并成一个函数,以参数的形式传入不同的值,从而消除重复的逻辑。此重构方法能够使得逻辑更加简洁、复用性强,因为每个函数都可以进行多次使用。
举个栗子
- //原始逻辑
- function useFun(param){...}
- function baseFunction(param){
- if(param < 0) return useFun(param);
- const amount = bottomFun(param) * 0.1 + middleFun(param) * 0.2 + topFun(param) *0.3;
- return useFun(amount);
- }
- function bottomFun(param){
- return Math.min(param,100)
- }
- function middleFun(param){
- return param > 100 ? Math.min(param,200) - 100 :0;
- }
- function topFun(param){
- return param > 200 ? param - 200 : 0;
- }
- //重构代码
- function commonFun(param,bottom,top){
- return param > bottom ? Math.min(param,top) - bottom:0;
- }
- function baseFun(param){
- if(param<0) return useFun(0);
- const amount = commonFun(param,0,100) * 0.1 + commonFun(param,100,200) * 0.2 + commonFun(param,200,Infinity) *0.3;
- return useFun(amount);
- }
3. 移除标记参数
标记参数直接理解就是作为标记的参数,即通常调用者用其来只是被调用函数应该执行哪部分逻辑。但事与愿违,标记参数在实际使用过程中并没达到作为标记的作用,令人难以理解到底哪部分函数可以调用、应该如何调用。通常我们通过API查看哪部分是可调用函数,但是编辑参数却会进行隐藏函数调用中存在的差异性,在使用这些函数我们还得阅读上下文中标记参数有哪些可用的值。
要知道布尔值作为标记是多么荒唐的使用方法,因为其不能见名知意的传递信息,在函数调用时很难厘清true代表的含义,但是明确使用函数完成单独的任务,就显得清晰的多。
当然并非所有的类似参数都是标记参数,如果调用者传入的程序中不断传递的数据,那么这样的参数就不叫做标记参数。只有当调用者初入字面量值时,或者在函数内部只有参数影响了函数内部的控制流,此时作为参数就是标记参数。
移除标记参数不仅使得代码更加整洁,并且能够帮助开发工具更好的发挥作用。去掉标记参数后,代码分析工具能够更清晰体现“高级”和“普通”逻辑在使用时的区别。如果某个函数有多个标记参数,此时想要移除得花费功夫,得不偿失还不如将其保留,但是也侧面证明此函数做的太多,需要将其逻辑进行简化。
举个栗子
- //原始代码
- function setFun(name,value){
- if(name === "height"){
- this._height = value;
- return;
- }
- if(name === "width"){
- this._width = value;
- return;
- }
- }
- //重构代码
- function setHeight(value){
- this._height = value;
- }
- function setWidth(value){
- this._width = value;
- }
4. 保证完整性
当看到代码从一个记录结构中导出几个值,然后又把这几个值传递给一个函数,那么可以把整个记录传递给这个函数,在函数内部导出所需要的值。
- //原始代码
- const low = aRoom.dayRange.low;
- const high = aRoom.dayRange.high;
- if(plan.goodRange(low,high)){...}
- //重构代码
- if(plan.goodRange(aRoom.dayRange)){...}
5. 以查询取代参数
函数的参数列表应该总结该函数的可变性,标识出函数可能体现出行为差异的主要方式,但是参数列表又应该尽量避免冗余,因为短小精悍易理解。什么是冗余,就是倘若调用函数中传入一个值,而这个值由函数自己获取,这个本不必要的参数会增加调用者的难度,因为调用者不得不去找出此参数定义的位置。
如果想要移除得参数值只需要向另一个参数值查询即可得到,这就可以使用以查询代替参数;如果在处理的函数具有引用透明性,即在任何时候只要传入相同的参数值,该函数的行为永远一致,可以让它访问一个全局变量。
6. 以参数取代查询
在浏览函数实现时,会经常发现一些糟糕的引用关系,比如引用一些全局变量或者另一个想要移除得元素,其实可以通过将其替换成函数参数来解决,将处理引用关系的责任推卸给函数调用者。其实此重构思想是:改变代码的依赖关系,让目标函数不再依赖某个元素,将元素的值以参数形式进行传递给函数。当然,如果把所有依赖关系都变成参数,会导致参数列表冗长重复,其次倘若作用域间的共享太多,会导致函数间过度依赖。
具体做法:将执行查询操作的代码进行变量提炼,将其从函数体中分离,对现有函数体代码不再执行查询操作,而是使用上一步提炼的变量,对此部分代码使用函数提炼。使用内联变量就是把提炼出来的变量放到一个函数中,且对原先的函数使用内联函数。
- targetFun(plan)
- const otherFun = {...}
- function targetFun(plan){
- curPlan = otherFun.curPlan
- ...
- }
- //重构
- targetFun(plan)
- function targetFun(plan,curPlan){
- ...
- }
7. 移除设值函数
当为某个字段提供了设置函数,表示此字段被改变,如果不希望在对象创建之后字段被改变,就不要提供设值函数,同时声明此字段不可改变。但是呢,有些开发者喜欢通过访问函数来读取字段值,在构造函数内也是,这就会导致构造函数成为设值函数的唯一使用者,就这样你还不如直接移除设值函数呢,没有意义。
当然,对象有可能是由客户端通过脚本(通过调用构造函数,即一系列的设置函数)进行构造出来的,而不是只有一次简单的构造函数调用。在执行完创建脚本后,此新生对象的部分字段不应该再被修改,设值函数只能被允许在起初对象创建过程中被调用。其实此时也应该移除设值函数,能够更加清晰的表达意图。
- class User{
- get(){...}
- set(){...}
- }
- //重构
- class User{
- get(){...}
- }
8. 以工厂函数取代构造函数
很多面向对象语言都有构造函数用于对象的初始化,通常客户端会通过调用构造函数来新建对象。但对于普通函数而言,构造函数具有一定的局限性,通常只能返回当前所调用类的实例,就是无法根据环境或参数信息返回子类实例或代理对象。且构造函数名字是固定的,因此无法使用比默认名字更清晰的函数名,此外还需要通过特殊的操作符(关键字new)来创建实例调用。然而,工厂函数就不受限制,可以实现内部调用构造函数,也可以使用其他方式调用。
9. 以命令取代函数
函数可以是作为独立函数,也可以作为类对象中的方法,还是作为程序设计的基本构造模块。将函数封装成自己的对象成为命令对象,当然这种对象大多只服务于单一函数,获得该函数的请求并进行执行函数,就是这种对象存在的意义。
与普通函数相比,命令对象提供了更加强大的控制灵活性和更强的表达能力,除了函数调用本身,命令对象还可以作为支持附加的操作,比如撤销。可以通过命令对象提供的方法进行设置和取值操作,从而提升丰富的生命周期管理能力。
具体方法:为想要包装的函数创建一个空类,并根据该函数的名字命名类,将函数搬移到空类中,并对每个参数创建一个字段,在构造函数中添加对应的参数。
举个栗子
- //原始代码
- function user(name,work,address){
- let result = "";
- let addressLevel ="";
- ...long code
- }
- //重构代码
- class User{
- constructor(name,work,address){
- this._name = name;
- this._work = work;
- this._addrsss = address;
- }
- clac(){
- this._result="";
- this._addressLevel ="";
- ...long code
- }
- }
10. 以函数取代命令
命令对象为处理复杂计算提供了强大的机制,可以轻松将原本复杂的函数拆分成多个方法,彼此之间通过字段进行状态共享。拆分后的方法可以分别进行调用,开始调用之前的数据状态也可以逐步构建,但是这种强大功能是有代价的。通常我们调用函数让其完成自身的任务,当此函数不是很复杂时,命令对象显得得不偿失,还不如使用普通函数呢。
通常的,将创建并执行命令对象的代码单独提炼到独立函数中,对命令对象在执行阶段用到的函数逐一使用内联函数。使用改变函数声明,将构造函数的参数转移到执行函数。对于所有的字段在执行函数中找到引用它的地方,并将其改为使用参数,将调用构造函数和调用执行函数两步进行内联到调用函数中。
举个栗子
- //原始代码
- class ChargeClass{
- constructor(custom,param){
- this._custom = custom;
- this._param = param;
- }
- clac(){
- return this._custom.rate * this._param
- }
- }
- //重构代码
- function charge(custom,param){
- return custom.rate * param;
- }