前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >可读代码编写炸鸡九 - 抽取子问题

可读代码编写炸鸡九 - 抽取子问题

作者头像
syy
发布2020-07-09 11:50:43
5670
发布2020-07-09 11:50:43
举报
文章被收录于专栏:多选参数多选参数

大家好,我是多选参数的一员 —— 大炮。

原本想专门写个第三层的简介,但篇幅过短,也不会多少人看,就在本篇炸鸡简略提一下,然后便进入第三层的第一篇炸鸡就好。

我们回顾一下,可读代码编写第二层主要是讲了 代码逻辑 上的优化。

第三层便是更大的范围了,是关于代码组织,可以说是函数级别的代码组织优化

其实第三层还是可以分成两个部分:

  • 当你面对已经出现的不好代码该咋办
  • 在你写代码前如何尽量避免不好代码

废话不多说,先上一个第三层的概览导图:

后台回复 「第三层」获取源文件

所以本篇炸鸡从 抽取不相关的子问题 这一点入手可读代码编写。

写在前头

对于代码的编写,大事化小,小事化了。模块化是我们很需要的一个思想。

本篇炸鸡主要是讲 提取无关的子问题

公众号后台回复 「抽取」获得源 pdf 文件

为了达成这个目标,来个自问三连。

  1. 看一下整个代码块或者一个函数,问一下自己,这个代码的 最终目的 是什么?
  2. 对于代码的每一行,问一下自己,这样对这个目标有 直接作用 吗。还是这代码用于解决其他的不相关的子问题
  3. 如果解决不相关的子问题的代码开始变多,问一下自己,是不是需要提取,封装为函数

本炸鸡主要关注:不相关的子问题的提取。所以本炸鸡需要提供不同条件下的例子,来阐述提取的技术。

而这个不相关的子问题,并不是和代码目的毫无干系的代码块,而是 篇幅过大,但解决的问题只是很小一部分 的代码块。

芜湖,如何抽取

首先要抽取不相关的子问题之前,得知道啥是不相关的子问题?

可以参考在上文的自问三连中的第一问:

代码语言:javascript
复制
这个代码的最终目的是什么

也就是与最终目的相关性不高的代码,都可以算作不相关的子问题,是附着代码。

我们看一段代码:

代码语言:javascript
复制
function wananDoA()
    -- do something about a
    ...
    -- do something about b
    ...
    ...
    ...
    ...
    ...
    -- do something about c
    ...
    ...
    ...
    ...
    ...
    
    -- do something about a
    ...
end

我用了一个比较抽象的代码,这段代码目标是 wananDoA,也就是做 A 相关的事情,但是函数里充斥着大量的解决其他子问题的附着代码 BC

所以就需要将 BC 相关代码抽取,比如这样:

代码语言:javascript
复制
function wananDoB()
    -- do something about b
    ...
    ...
    ...
    ...
    ...
end

function wananDoC()
    -- do something about c
    ...
    ...
    ...
    ...
    ...
end

function wananDoA()
    -- do something about a
    ...
    wananDoB()
    wananDoC()
    -- do something about a
    ...
end

我们可以发现,抽取完的代码整体:

  • 不占用调用函数的篇幅
  • 专注调用函数目标
  • 被抽取代码拥有更好的拓展性,解耦合

其实抽取子问题的核心实现就是这样而已。今天的炸鸡到此结束,谢谢大家。

我开玩笑的兄弟,再接一些具体的抽取方法吧。

工具代码

大概各位的项目中都会有一个 「工具人」模块。就是存放一些工具代码,供各开发人员使用。

例如,字符串操作,日期操作等等,这些完全可以抽取为工具代码。

如下代码是游戏跨天要做的事情:

代码语言:javascript
复制
function onDayPass()
    -- 获取玩家数据
    ...
    ...
    
    -- 结算
    ...
    ...


    -- 数据落地
    ...
    ...
end

如果再结算的逻辑中,需要判断玩家的活跃时间是否在指定的时间区间内,同时需要知道玩家的最近上线时间与上一次离线时间是否在同一天。

代码语言:javascript
复制
function onDayPass()
    -- 获取玩家数据
    local user = getUser()
    ...
    ...
    
    -- 结算
    ...
    
    -- 指定区间    
    local beg, ending = readActiveIntervalConfig()
    local curTime = os.time()
    if curTime > ending then return false end
    if curTime < beg then return false end
    
    
    -- 上下线时间是否同一天
    local elapse = user.logoutTime - user.loginTime >= 0 and user.logoutTime - user.loginTime or user.loginTime - user.logoutTime
    if elapse > 86400 then
        return false
    end
    if os_date("%j",user.logoutTime ) != os_date("%j",user.loginTime) then
        return false
    end
    ...
    ...

    -- 数据落地
    ...
    ...
end

很明显,上述代码的结算逻辑中,判断时间区间,是否同一天等一系列逻辑都是 篇幅大,但是解决的问题是很小的 代码逻辑,也是复用性很强的代码,也就是前头说的 实用工具代码

这些正是 相关性较小的子问题

我们将其分出来。

代码语言:javascript
复制
-- 当前时间是否在一个区间内
function isInInterval(beg, ending)
    local curTime = os.time()
    if curTime > ending then return false end
    if curTime < beg then return false end
end

-- 绝对值
function mathAbs(t1, t2)
    return t1 - t2 >= 0 and t1 - t2 or t2 - t1
end

-- 是否在同一天
function isSameDay(t1, t2)
    if mathAbs(t1, t2) > 86400 then
        return false
    end
    
    if os_date("%j",t1 ) != os_date("%j", t2) then
        return false
    end
    
    return true
end

于是原本的代码被简化为:

代码语言:javascript
复制
function onDayPass()
    -- 获取玩家数据
    local user = getUser()
    ...
    ...
    
    -- 结算
    ...
    -- 指定区间    
    local beg, ending = readActiveIntervalConfig()
    if not isInInterval(beg, ending) then
        return false
    end
    
    -- 上下线时间是否同一天
    if not isSameDay() then
        return false
    end
    ...
    ...

    -- 数据落地
    ...
    ...
end

芜湖,多了几个工具代码,还缩小了函数篇幅,岂不美哉。

简化已有接口

什么是已有接口呢。其实这也有两个情况。

一个是语言层面已有的接口不能适应项目需求,导致一些函数体量得到不必要的增加。

我拿 lua 举例,lua 中只有一个复杂的数据结构叫做 tabletable 有相应的 table 库对其操作,但是不存在一个清空操作。如果在一串代码中,我需要清空一个 table,那我得写这样的代码:

代码语言:javascript
复制
for key, _ in pairs(t) do
    t.key = nil
end

所以我还不如对 table 这个库再加一个 clear 方法。

代码语言:javascript
复制
table.clear = function(t)
    for key, _ in pairs(t) do
        t.key = nil
    end
end

还有一种情况,面对现有接口,参数过多的情况:

代码语言:javascript
复制
function interface(uid, aid, params, isSave, ...)
    ...
    ...
    ...
end

针对现有业务功能,完全可以在此接口上再封装一层接口:

代码语言:javascript
复制
function saveInterfaceWithRewardParams(uid, aid, ...)
    local params = {
        id     = 1001,
        type   = 1,
        amount = 3,
    }
    return interface(uid, aid, params, true, ...)
end

所以面对已有的接口,完全可以再进行包装。对于代码可读性来说,还是对调用的方便来说,都是有裨益的。

而且其实在调用方便这一层面,利用函数合适的命名,也是增强了代码的可读性。

不要过分抽取

看完前面的内容,最怕的情况是过度抽取。

看一下如下的抽象代码:

代码语言:javascript
复制
function _doA()
    ...
end

function _doB()
    ...
end

function _doC()
    ...
end

function _doD()
    ...
end

function _doE()
    ...
end

function wannaDoA()
    _doA()
    _doB()
    _doC()
    _doD()
    _doE()
end

我们可以看到,抽取了多个子问题,反而需要我们不断地跳转查看具体逻辑,反而增加了思想包袱,违背 明确函数目标 的初心。

总结

好了,这篇炸鸡算是到结尾了,其实这个炸鸡最关键的就是。

  1. 阅读者注意力。
  2. 函数的目的。
  3. 篇幅。

所以为了使阅读者专注于函数的目的,我们需要将对于目的而言 相关性较小的子问题 抽取出来,变成一个独立的函数,甚至是库。这个取决于这个子问题使用的范围是一个文件,还是一整个项目。

本篇炸鸡基于此,罗列了许多抽取不同子问题的不同情况,但是总而言之,便是合理地将代码块抽取和封装。

不甘于「本该如此」,「多选参数 」值得关注

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2020-07-06,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 多选参数 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 写在前头
  • 芜湖,如何抽取
    • 工具代码
      • 简化已有接口
      • 不要过分抽取
      • 总结
      领券
      问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档