分解条件及其权衡
#重构 #go #codequality #cleancode

分解条件是一种常见的重构策略。
主要的想法是,复杂的if... else...语句很难遵循和理解,并且可以分解为较小的零件。但这并非没有成本。


不具有多行条件,而是分解条件模式指出,应将条件逻辑提取到3种单独的方法,其中一个用于语句:一个适用于条件本身,一个用于“真”分支和一个分支对于“ false”分支。

例如,如果您的代码看起来像这样:

....
if baby.IsAwake() && baby.State == "Crying" && baby.TimeSinceLastMeal > 3 * time.Hour {
    if baby.Diaper.IsDirty() {
        baby.ChangeDiaper()
    } 
    bottle := NewBabyBottle()
    bottle.FillWater()
    bottle.AddMilkFormula()
    bottle.Shake()
    baby.Feed(bottle)
    baby.Burp()
} else {
    baby.Cuddle()
    baby.Play()
}
....

分解条件意味着重构您的代码看起来像这样:

....
if baby.IsHungry() {
    baby.ChangeAndFeed()
} else {
    baby.FunTime()
}
....
func (baby *Baby) IsHungry() {
    return baby.IsAwake() && baby.State == "Crying" && baby.TimeSinceLastMeal > 3 * time.Hour
}

func (baby *Baby) ChangeAndFeed() {
    if baby.Diaper.IsDirty() {
        baby.ChangeDiaper()
    } 
    bottle := NewBabyBottle()
    bottle.FillWater()
    bottle.AddMilkFormula()
    bottle.Shake()
    baby.Feed(bottle)
    baby.Burp()
}

func (baby *Baby) FunTime() {
    baby.Cuddle()
    baby.Play()
}

这种更改使主要方法流动更容易理解和遵循,从而消除可能与读者并不总是相关的复杂逻辑。

将逻辑提取到单独的方法还可以使我们将来更轻松地重复使用或重新分配代码,因为逻辑更好地封装了。例如,一旦我们的宝宝开始吃固体食物,我们就可以修改ChangeAndFeed方法以轻松反映这种方法,而无需触摸任何与此更改无直接相关的代码,如果检查婴儿是否逻辑,则相同饥饿的变化 - 例如,一旦我们的婴儿开始以4小时的间隔进食。

分解条件的另一个好处是,我们现在可以为分解方法编写单元测试,以确保我们的逻辑正确,并且由于将来的更改不会破裂。


如果分解条件是如此之大,那么为什么我们不总是这样做?
像软件工程中的所有内容一样,都有一个权衡。

将逻辑提取到单独的方法时,还有其他开销。虽然大多数语言的函数调用开销很少(在非常极端的情况下,例如深层递归或严格的资源约束环境),而代码读取器的开销不是。软件工程师花费的时间比编写代码要多得多,我们应该始终考虑如何使代码更可读。在方法定义和呼叫之间来回跳动为有兴趣了解代码做什么的读者增加了复杂性,例如在审查或调试时。

以以下代码为例。哪个版本更容易理解和遵循,这是:

if len(orgIDs) > maxItems || len(userIDs) > maxItems {
   return errors.BadRequest("Too Many Items")
}

还是这个?

if validateIDsSize(orgIDs, userIDs) {
    return tooManyItems()
}
....
func validateIDsSize(ids ...[]string) bool {
    for _, idGroup := range ids {
        if len(idGroup) > maxItems {
            return true
        }
    }
    return false
}

func tooManyItems() error {
    return errors.BadRequest("Too Many Items")
}

此处的关键字是复杂性。我们应该问的问题是:“这件代码是否足够复杂,可以将其提取到单独的方法中会使它更可读吗?”
如果答案是该代码足够清楚,可以理解内联,我们应该避免将其分解为单独的功能,除非有很好的理由这样做 - 例如需要重复使用或测试。


第二个示例还显示了我们在将逻辑提取到单独方法时面临的另一种常见风险:过早尝试概括。虽然第一个版本仅检查两个特定值,但第二个版本试图创建一个可以接受任何数量值的通用函数。

检查的通用版本创造了其他复杂性来处理所有可能的情况,而不是更简单的检查来处理我们面临的特定情况。
如果我们有许多类似的情况并检查了不同的值,那么创建一个处理所有情况的通用函数可能是有意义的。
但是,我们经常发现仅在一个或两个地方使用的通用函数 - 在这种情况下 - 与非传播版本相比,附加的复杂性通常是没有回报的。

我们也不知道所有潜在的未来案件都会表现不佳。例如,如果将来不同的值将具有不同的限制怎么办?通用实现假设所有值都受maxItems的限制 - 意味着如果此假设发生了变化,则此方法将变得更加复杂。当我们发现新的要求时,非一般实施更容易更改。


正确使用时,分解条件是一种有用的重构方法,可以使我们的代码易于理解,更易于测试和更重复使用。但是,在我们进行重构代码之前,我们必须始终牢记我们的最终目标,并考虑每种方法中可能的权衡。