写点什么

重构的艺术:在代码演进中寻找优雅

  • 2025-03-03
    福建
  • 本文字数:5645 字

    阅读完需:约 19 分钟

1 重构


重构没有那么复杂,重构是我们的日常工作,就像吃饭,就像喝水。重构是有时机的,就像我们的一日三餐,时机对了,事半功倍,时机不对,事倍功半。在我们的开发工作中,重构的时机俯仰皆是。


2 重构的时机: 功能重复时


如果发现代码的功能重复,这就是重构的时机,这样的时间经常有,只要开发者心中有重构在这根弦。


2.1 案例初始状态


例如,最近我在学习微信小程序开发,涉及到页面跳转,在首页中,index 页面的 index.js 代码如下:



Page({ gotoCollection() { wx.navigateTo({ url: '/pages/collection/collection', }) }, gotoActivity() { wx.switchTab({ url: '/pages/activity/activity', }) }, gotoFace() { wx.navigateTo({ url: '/pages/face/face', }) }, gotoVoice() { wx.navigateTo({ url: '/pages/voice/voice', }) }, gotoHeart() { wx.navigateTo({ url: '/pages/heart/heart', }) }, gotoGoods() { wx.navigateTo({ url: '/pages/goods/goods', }) },
})
复制代码


  • 这段代码的意思是:如果触发 Collection 事件,跳转到 collection 页面,如果触发 gotoActivity 事件,跳转到 activity 页面,如果触发 gotoFace 事件,跳转到 face 页面这段代码是在首页的 js 文件中的,你发现了重构的时机了吗

  • 分析跳转页面有规律:/pages/[action]/[action]wx.navigateTo 出现了多次 wx.navigateTo 和 wx.switchTab 含义明确,可以用一句话命数:跳转到某个页面(face)跳转到某个 Tab 页(activity)

  • 结论

  • 基于以上认识,我觉得需要重构抽象通用函数:页面跳转,且页面符合:/pages/action/action


2.2 重构实现


基于上面的理解,重构后的 index 页面的 index.js 代码如下


Page({  jumpPage(tag) {   // 跳转到特定页面    wx.navigateTo({      url: '/pages/' + tag + '/' + tag,    })  },  switchTab(tag) { // 切换到特定Tab    wx.switchTab({      url: '/pages/' + tag + '/' + tag,    })  },  gotoCollection() {    this.jumpPage('collection')  },  gotoActivity() {    this.switchTab("activity")  },  gotoFace() {    this.jumpPage('face')  },  gotoVoice() {    this.jumpPage('voice')  },  gotoHeart() {    this.jumpPage('heart')  },  gotoGoods() {    this.jumpPage('goods')  },
})
复制代码


2.3 重构分析


  • 抽象出 jump 和 switchTab, 写代码时,细节更少, 更符合人的思维习惯

  • 例如: 跳转到 face 页面:this.jump("face")

  • 隐藏了实现细节:wx.navigateTo, wx.switchTab, url: '/pages/' + tag + '/' + tag

  • 更适合特定环境:'/pages/' + tag + '/' + tag 可能是特定场景的规则

  • 对实现的妥协(适用环境缩小:80%原则)


2.4 重构的其他收益


重构的收益,除了

  • 减少代码量

  • 突出主要逻辑: 更符合人类思维(AI?)


还有其他收益吗

  • 通用函数的可替代性:隐藏细节

  • 以 jumpPage 为例, wx.navigateTo 是我们的实现手段,属于细节

  • 我们抽象出 jumpPage 后,如果觉得 wx.navigateTo 不合适,或 wx.navigateTo 升级为新的方法,修改 jumpPage 即可,不需要修改 gotoFace 等

  • 易于扩展,修复错误

  • 例如: 我想在跳转前后,打印信息,重构前,需要一个一个修改,重构后,只需要修改 jumpPage

  • 这一点对修复错误,扩展功能更有利

  • 下面是添加打印信息后的 index 页面的 index.js 代码, 通常在调试时适用:


Page({  jumpPage(tag) {   // 跳转到特定页面    console.log(tag) // 打印tag    wx.navigateTo({      url: '/pages/' + tag + '/' + tag,    })    console.log('jumpPage:/pages/' + tag + '/' + tag) // 打印url  },  switchTab(tag) { // 切换到特定Tab    console.log(tag) // 打印tag    wx.switchTab({      url: '/pages/' + tag + '/' + tag,    })    console.log('switchTab:/pages/' + tag + '/' + tag) // 打印url  },  gotoCollection() {    this.jumpPage('collection')  },  gotoActivity() {    this.switchTab("activity")  },  // ...
})
复制代码


2.5 重构只是开始:进一步重构


  • 抽象出 jumpPage 后,思考其他页面是否面临页面跳转,如果是,只放在首页是否合适

  • 更适合抽象为全局函数,放在 app.js 中

  • 将通用函数放到 app.js 中,多个页面可复用


App({  jumpPage(tag) {   // 跳转到特定页面    console.log(tag) // 打印tag    wx.navigateTo({      url: '/pages/' + tag + '/' + tag,    })    console.log('jumpPage:/pages/' + tag + '/' + tag) // 打印url  },  switchTab(tag) { // 切换到特定Tab    console.log(tag) // 打印tag    wx.switchTab({      url: '/pages/' + tag + '/' + tag,    })    console.log('switchTab:/pages/' + tag + '/' + tag) // 打印url  },})
复制代码


index 的 index.js 修改后如下:


Page({  gotoCollection() {    const app = getApp()    app.jumpPage('collection')  },  gotoActivity() {    const app = getApp()    app.switchTab('activity')  },  // ...})
复制代码


重构后的 jumpPage, switchTab 其他页面也可以适用,但也提出要求,跳转 url 必须符合 '/pages/' + tag + '/' + tag规范


3 重构的时机: 设计接口时


在微信小程序编程时,加载页面是一个特定的场景,逻辑描述如下:



3.1 通常的实现


下面这段代码是 collection 页面的一段正常逻辑


// ...Page({    // ...  onLoad() {    // 加载    wx.showLoading({      mask: true    })    wx.request({      url: api.collection,      method: 'GET',      success: (res) => {        if (res.data.code == 100) {          this.setData({            dataDict: res.data          })        } else {          wx.showToast({            title: '网络加载失败',          })        }      },      complete: () => {        wx.hideLoading()      },    })  },  // ...})
复制代码


3.2 代码分析


分析这段代码后,发现:


  • 多数逻辑具有通用性

  • 这段代码中,多数代码具有通用性,例会:wx.showLoading, wx.hideLoading, 发送 request 请求,请求成功处理,请求失败处理


  • 部分逻辑具有独特性

  • 这段代码中,只有部分逻辑不同,url,success 的处理


  • 暴露了过多细节 wx.showLoading,

  • 这段代码中暴露了过多的实现细节,例如 wx.request,wx.showLoading,wx.hideLoading


  • 不方便测试

  • 通过上述分析,发觉这是一个通用场景,不仅仅适用于一个页面,可以适用于多个页面。

  • 如果对测试要求严格,为每个页面提供单独测试,很不经济。


3.3 问题追寻


上述代码无疑是正确的,但存在问题。我们一开始就陷入细节,而没有进一步思考。这是一个接口,符合 3.1 中的逻辑。


3.3.1 抽象通用接口


3.1 中的逻辑分析下来,就是如下的接口,


void onLoad(url, onSuccess);
复制代码


用语言描述就是:请求某个 url,如果请求成功,则按 onSucess 的方法进行响应,如果失败,则按默认处理


3.3.2 抽象接口利于测试


3.3.2.1 测试用例


针对上述接口,我们可以很方便的写出测试用例测试用例主要有两个:


1.可以请求成功的 url,onSucess 会被调用

2.请求失败的 url,onSucess 不会调用, 甚至可能 onFailed 被调用


大家还能想出第三种情况吗,请思考根据上面的测试用例,可以修正接口:


void onLoad(url, onSuccess, onFailed);
复制代码


3.3.2.2 妥协


按照测试用例的设置,包含 onFailed 的接口无疑是好的


如果考虑到我们的情况有限,void onLoad(url, onSuccess)也可以接受,这就是需要在理想与显示之间做选择和妥协


如果是我设计接口,我会这样设计


function defaultFunc() {}void onLoad(url, onSuccess, onFailed=defaultFunc);
复制代码


这样,既可以方便测试,使用时,如果不需要处理 onFailed, 可以忽略。


3.3.2.3 接口定义


下面是重新定义的接口,放在全局位置 app.js


App({    // 修正函数名拼写错误    defaultRequestFailed(error) {        console.log("load failed:" + error);        wx.showToast({            title: '网络加载失败',        });    },    loadPageByGetRequest(url, onSuccess, onFailed = this.defaultRequestFailed) {        wx.showLoading({            mask: true        });        wx.request({            url: url,            method: 'GET',            success: (res) => {                if (res.data.code == 100) {                    // 修正回调函数调用                    onSuccess(res);                } else {                    onFailed("res code is failed");                }            },            // 修正事件名            fail: (error) => {                onFailed(error);            },            complete: () => {                wx.hideLoading();            },        });    }});
复制代码


3.3.2.4 结论


在编程时,最佳的重构时机就是设计接口的时候,考虑通用性,考虑可测试行,考虑依赖性,考虑放置的位置,这样设计的接口,可以方便的被适用,被测试,且对外部依赖较少,也有利于后续的演化


3 重构的时机:编写复杂函数时


在写一个函数的时候,很容易发现重构的时机。如果觉得函数做了好几件事情,就说明需要重构了。如果函数长度比较长,就说明需要重构了。例如: 下面是一段人脸识别的代码,拍照,将拍照图片传到后台,并将识别结果返回处理的函数:


  takePhoto(e){    // 1 打开loading    wx.showLoading({      title: '检测中',      mask:true    })    //2 拿到相机对象,拍照    const ctx = wx.createCameraContext()    ctx.takePhoto({      quality: 'high',      success: (res) => {        //3 res中会有拍摄的照片
// 4 把照片传到后端 wx.uploadFile({ url: api.face, filePath: res.tempImagePath, name: 'avatar', success:(response)=>{ // 5 上传成功,后端返回数据 let resdata = JSON.parse(response.data) if(resdata.code==100 || resdata.code==102){ resdata.avatar = res.tempImagePath var oldRecord = this.data.record oldRecord.unshift(resdata) console.log(oldRecord) this.setData({ record:oldRecord })
}else{ wx.showToast({ title: '请正常拍照' }) } }, complete:function(){ wx.hideLoading() } }) } }) }
复制代码


这一段代码挺长,做了好几件事情:

  • 拍照识别时,打开 loading 页面,完成后,关闭 loading 页面

  • 拍照

  • 拍照成功后,对结果进行处理

  • 可以根据上面的功能,将函数进行拆分重构为:

  • 拍照成功后,对结果进行处理的函数


onUploadPhotoSuccess(path, response) {    let resdata = JSON.parse(response.data)    if (resdata.code != 100 && resdata.code != 102) {        wx.showToast({            title: '请正常拍照'        })        return    }    resdata.avatar = path    var oldRecord = this.data.record    oldRecord.unshift(resdata)    console.log(oldRecord)    this.setData({        record: oldRecord    })}
复制代码


takePhoto 函数已经抽象的很好了,保持原样就可以了。这样重构后的好处:

  • 每个函数代码更少,逻辑更突出

  • 每个重构后的函数,依赖性更小,更易于测试

  • 例如 onUploadPhotoSuccess 摆脱了对 Camera 的硬件依赖

  • 更利于发现问题

  • 重构后,就会发现 takePhoto 没有考虑到 failed 情况,可能会出现 Loading 页面不会退出的情况。

  • 需要在代码中,添加相应的处理


完整代码:


    onUploadPhotoSuccess(path, response) {        // 5 上传成功,后端返回数据        let resdata = JSON.parse(response.data)        if (resdata.code != 100 && resdata.code != 102) {            wx.showToast({                title: '请正常拍照'            })            return        }        resdata.avatar = path        var oldRecord = this.data.record        oldRecord.unshift(resdata)        console.log(oldRecord)        this.setData({            record: oldRecord        })    },    onTakePhoto(res) {        //3 res中会有拍摄的照片        // 4 把照片传到后端        wx.uploadFile({            url: api.face,            filePath: res.tempImagePath,            name: 'avatar',            success: (response) => {                // 5 上传成功,后端返回数据                this.onUploadPhotoSuccess(res.tempImagePath, response)            },            complete: function () {                wx.hideLoading()            }        })    },    takePhoto(e) {        // 1 打开loading        wx.showLoading({            title: '检测中',            mask: true        })        //2 拿到相机对象,拍照        const ctx = wx.createCameraContext()        ctx.takePhoto({            quality: 'high',            success: (res) => {                this.onTakePhoto(res)            },            fail: () => {                // 拍照失败,隐藏加载提示                wx.hideLoading();                wx.showToast({                    title: '拍照失败,请重试',                    icon: 'none'                });            }        })    }})
复制代码


文章转载自:荣--

原文链接:https://www.cnblogs.com/Rong-/p/18744215

体验地址:http://www.jnpfsoft.com/?from=001YH

用户头像

还未添加个人签名 2023-06-19 加入

还未添加个人简介

评论

发布
暂无评论
重构的艺术:在代码演进中寻找优雅_重构_快乐非自愿限量之名_InfoQ写作社区