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

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页面的一段正常逻辑wcollection.js:

// ...
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'
                });
            }
        })
    }
})
posted @   荣--  阅读(161)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· 阿里巴巴 QwQ-32B真的超越了 DeepSeek R-1吗?
· 如何调用 DeepSeek 的自然语言处理 API 接口并集成到在线客服系统
· 【译】Visual Studio 中新的强大生产力特性
· 2025年我用 Compose 写了一个 Todo App
点击右上角即可分享
微信分享提示