欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

日常代码重构笔记

程序员文章站 2022-05-19 17:07:56
...

这里存放一些平时看代码时觉得需要重构的地方并记录下来(语言可能不止一种),供大家参考|讨论。

1. 数据容器一致性操作|函数单一职能

功能需求:对多个节点布局时,如果节点排布超出屏幕则将预定节点调整至一个可滑动容器中(scrollUI);
数据容器:1)originalNodeLst : 存放原始节点;2)showedNodeLst :显示出来的第一层节点,视窗节点布局时使用该数据容器,当节点未被移至scrollUI时,该容器与originalNodeLst保持一致;3)nodeMap:节点名称与节点对象的映射,用于基于名字的节点检索;
重构前代码

function T:getScrollUI()
    local cs = cc.size(0,0)
    local scrollUI = createScrollUI() -- 每次都创建一个新的
    for k,v in pairs(originalNodeLst) do
        local hasGot = false -- 标记是否已经找到需要移动到scroll中的原始节点
        for k1,v1 in pairs(preDefNodeName) do
            if not hasGot then
                if v == nodeMap[v1] then
                    hasGot = true
                    moveToScrollUI(v,scrollUI) -- 将原始节点移至scrollUI
                end
            end
        end
    end

    if self.scrollUI then -- self.scrollUI额外保存了scrollUI
        self.scrollUI:removeFromParent() -- 移除旧的scrollUI
        self.scrollUI = nil
    end

    self.scrollUI = scrollUI
    self.scrollUI:setContentSize(self.scrollUI:getInnerContainerSize())
    self.scrollUIParent:addChild(self.scrollUI)
    return self.scrollUI
end

function T:adaptLayout()
    local viewContSize = self:getOriViewSize() -- 原始视窗大小
    if viewContSize.height > maxViewHeight then -- 原始视窗大小超出最大限定范围,将一些节点搬移至scrollview中
        local subHeight = viewContSize.height - maxViewHeight -- 需要缩短的高度
        local scrollUI = self:getScrollUI()
        local scrollUIContSize = scrollUI:getContentSize()
        scrollUIContSize.height = scrollUIContSize.height - subHeight -- 需要缩短的高度体现在scrollUI中
        scrollUI:setContentSize(cc.size(scrollUIContSize.width,scrollUIContSize.height))

        self.showedNodeLst = nil -- 该容器存放视窗上显示的一级节点(原始节点中被移至scrollUI的为二级节点)
        local flag = false -- 是否已将scrollUI更新至容器中
        for k,v in pairs(originalNodeLst) do
            if self:isInScrollUI(v) then
                flag = true
                table.insert(self.showedNodeLst,scrollUI)
            else
                table.insert(self.showedNodeLst,v)
            end
        end
    end
    doLayout()
end

代码存在一下几个问题:
1)数据容器更新的不一致性:程序使用self.scrollUI单独保存了对象,而该对象在adaptLayout()中又被加入到了showedNodeLst数据容器中,那么后续析构该对象时总要坚固两部分,一不留神就产生了野指针;
2)函数职能不单一:adaptLayout()的工作是做界面布局,但上述程序还进行了数据容器更新的操作,很明显,这一操作应该在数据发生变化时完成,而不应该延迟到UI界面布局阶段;
3)冗余的标识性变量:上述代码用标志性变量干预了循环体执行流程,看起来不直观且易出错,可将其独立成简易的函数体以消除这些变量;
4)避免对象的重复构造:每次调用adaptLayout()时总会创建一个新的scrollUI,这样做效率不高,可将其保存至nodeMap映射容器中;
重构后的代码如下:

function getScrollUI()
    local scrollUI = nodeMap["scrollUI"]
    if scrollUI then return scrollUI end

    local isNeedMove = function ( node ) -- 封装成一个独立函数 避免使用标记变量控制函数流程
        for k,v in pairs(preDefNodeName) do
            if node == self.nodeMap[v] then
                return true
            end
        end
        return false
    end

    scrollUI = createScrollUI()
    local scrollUIShowedIdx = nil -- 记录scrollUI将存放在showedNodeLst中的位置
    for k,v in pairs(originalNodeLst) do
        if isNeedMove then
            local idx = table.findkey(showedNodeLst,v)
            table.remove(showedNodeLst,idx)
            scrollUIShowedIdx = scrollUIShowedIdx and scrollUIShowedIdx < idx and scrollUIShowedIdx or idx
            moveToScrollUI(v,scrollUI)
        end
    end
    scrollUI:setContentSize(scrollUI:getInnerContainerSize())
    nodeMap["scrollUI"] = scrollUI
    table.insert(showedNodeLst,scrollUIShowedIdx,scrollUI)
    self.scrollUIParent:addChild(scrollUI)
end
function T:adaptLayout() -- 职能分离,布局就只做布局的事
    local viewContSize = self:getOriViewSize() -- 原始视窗大小
    if viewContSize.height > maxViewHeight then -- 原始视窗大小超出最大限定范围,将一些节点搬移至scrollview中
        local subHeight = viewContSize.height - maxViewHeight -- 需要缩短的高度
        local scrollUI = self:getScrollUI()
        local scrollUIContSize = scrollUI:getContentSize()
        scrollUIContSize.height = scrollUIContSize.height - subHeight -- 需要缩短的高度体现在scrollUI中
        scrollUI:setContentSize(cc.size(scrollUIContSize.width,scrollUIContSize.height))
    end
    doLayout()
end

总结:我们重构这部分代码,要怎么思考呢?很重要的一点是:我们要时刻关注我们的操作对已经定义好的数据结构|容器的影响,因为很多的野指针正是因为数据更新与容器操作不一致行造成的。如上例,当对节点变更后,最好紧随着更新其容器内容。而且这些容器一般都要编写统一的初始化|销毁接口。比如上述功能,会有一个init()函数将originalNodeLst、showedNodeLst及nodeMap全部置空。实际上我们在实现代码时应当时刻保持对数据容器的敏感性,避免随意添加变量来保存容器内数据引用。如果发现容器无法支撑要实现的逻辑,建议重新调整逻辑来适应我们既有的容器结构以保证程序的稳定性,随意修改、增加容器都不是明智之举。

2. 解开缠绕的嵌套逻辑分支

考虑一种需求:当进入某地图场景时需要隐藏一些按钮或是其他元素,当离开该场景时恢复。实现时只需要在进入时做标记,不在相关地图时判断是否被标记,如果已标记表明之前进入过地图,此时需要做一些恢复性工作。
如果情况变得更复杂一些,从当前地图进入一个属于该地图的小房间,也需要做类似的动作,从小房间进入更小的房间同样也有些类似的操作要完成,那么程序实现起来会时怎么样的呢?如下显示:

function doSomething()
    local hasEnterMap = false -- 标记是否进入过场景 下面类似
    local hasEnterSamllMap = false
    if isInMap() then
        hasEnterMap = true
        doSomethingForMap() -- 比如隐藏一些sprite icon等
        if isInSamllMap() then
            hasEnterSamllMap = true
            doSomethingForSmallMap()
        else
            if hasEnterSamllMap then -- 不在小房间 有可能是因为从小房间跳回至大房间 因此需要做检查 
                hasEnterSamllMap = false
                canclForSmallMap()
            end
        end
    else
        if hasEnterMap then
            hasEnterMap = false
            canclForMap() -- 恢复doSomethingForFstLvl的一些行为 如将隐藏的元素重新显示出来
            if hasEnterSamllMap then -- 前面不是已经检查了吗?为什么此处还要检查?因为有可能直接从小房间传送出了当前所在地图 而没有经过当前大地图
                hasEnterSamllMap = false
                canclForSmallMap()
            end
        end
    end
end

按照上面的写法,如果在有些逻辑继续嵌套下去,代码的可维护性将非常低下。上面之所以嵌套逻辑,是因为满足想要满足isInSamllMap条件必定先要满足isInMap条件,这么考虑,上面的逻辑是合理的,但是从职能角度考虑就不合理了。大房间负责做的事情可以完全与小房间独立开来,独立开来的益处就是格子处理自己的进出逻辑,而不用管其他的情形。
日常代码重构笔记

function doSomething()
    local hasEnterMap = false -- 标记是否进入过场景 下面类似
    local hasEnterSamllMap = false

    if isInMap() then
        hasEnterMap = true
        doSomethingForMap()
    else
        if hasEnterMap then
            hasEnterMap = false
            canclForMap()
        end
    end

    if isInSamllMap() then
        hasEnterSamllMap = true
        doSomethingForSmallMap()
    else
        if hasEnterSamllMap then
            hasEnterSamllMap = false
            canclForSmallMap()
        end
    end
end

上面的代码需要注意的是:将判断分支移出之后,每次执行doSomething都要执行isInSamllMap操作而不是仅在满足isInMap之后,因此最好对判断做些优化,比如采用map结构加速判断。

3. 寻求更贴合逻辑本质的控制变量

有这样一个需求:当接收到服务器下发的某一物品掉落信息时,播放特定的掉落飞行动画,下面是一种实现方式。

function server_addItem(item) -- 接收到相关掉落信息
    showDropEffect(item)
end
function showDropEffect(item)
    addToArr(item) -- 加入至容器
    if getArrItemNum() == 1 then
        local aniUI = getUI("aniUIName")
        aniUI.repeatAction(play) -- 重复播放特效
    else
        -- 如果容器中的道具数目不为1,说明aniUI应该还在循环执行play函数 
        -- 因此不需要执行播放动作
    end
end
function play()
    local num = getArrItemNum()
    if num > 0 then
        local item = getOneArrItem()
        playOneAni(item) -- 播放一次动画
        removeOneArrItem(item)
    else
        -- 容器中所有item已全部播完特效
        local aniUI = getUI("aniUIName")
        aniUI.stopAction()
    end
end

上面实现方法关键之处在于:判断容器中item数目是否为1,如果为1则此时道具为第一个道具,需要开启循环播放特效模式
这种做法的问题在于:如果aniUI的循环动作被异常停止(如界面被强制关闭),那么以后再掉落物品将不再播放特效,因为异常停止play函数后,容器中item数目始终大于1,启动循环播放动画的条件永远不会判过。
如何修改呢?实际上是否需要启动循环播放机制,本质在于当前的循环系统是否还在运作。如果未运作就开启,已运作则不做处理。重构后的代码如下:

function showDropEffect(item)
    addToArr(item) -- 加入至容器
    if not isPlaying then -- 循环机制已断 重新启动
        local aniUI = getUI("aniUIName")
        aniUI.repeatAction(play) -- 重复播放特效
    else
        -- 如果容器中的道具数目不为1,说明aniUI应该还在循环执行play函数 
        -- 因此不需要执行播放动作
    end
end
function play()
    local num = getArrItemNum()
    if num > 0 then
        isPlaying = true
        local item = getOneArrItem()
        playOneAni(item) -- 播放一次动画
        removeOneArrItem(item)
    else
        isPlaying = false
        -- 容器中所有item已全部播完特效
        local aniUI = getUI("aniUIName")
        aniUI.stopAction()
    end
end