处理有很多分支(if/then/else)的代码的技巧

4

我正在尝试重构一些有很多分支的代码,但遇到了困难。代码中有许多if/then/else块,其中一些是嵌套的。

有没有什么技巧可以用来重构代码,而不必浪费大量时间先理解功能的每个细节呢?

目前我主要使用布尔代数(德摩根定律)。我试图修改if语句中的条件,以便将小代码片段弹出if/then/else块外部。这在某种程度上有所帮助,但代码仍然分支很多。我知道在某个时候我最终将不得不将大块代码拆分成较小的类方法,但这很复杂,因为代码包含对许多其他类方法的调用,并且具有许多局部范围变量,因此我将不得不处理传递许多参数给新方法。我想知道在开始将其拆分为不同的小块(类方法)之前,是否可以使用其他技巧来改进代码质量。

4个回答

14
在我的经验中,在进入分支和等效逻辑表达式之前,将功能分解(将事物分解为执行一项任务并执行良好的较小函数)几乎总是更有效率的。
引用其他方面,功能分解的行为将导致您了解代码的作用,从而导致进一步的分解机会。您想获得只需少量参数并返回无个、一个或少量值的函数。
通过分解,您将更好地了解哪些部分在算法上是必要的(必须以正确的方式完成)以及哪些部分仅仅是实现细节(必须完成,但可以以各种不同的方式完成而不改变输出)。
通常,您会发现许多深嵌套的`if`语句是不必要的,或者是重复冗余的,或者足够相似,如果您因子出代码中的微小区别,则可以将它们缩减为一个或几个函数。
换句话说,您想掌握的不是代码的细枝末节,而是代码所处理的抽象内容。许多问题归结为某些数据结构(列表、队列、树、集合)和对该结构的某些操作。在您能够分离数据结构和算法的程度上,您可以获得简化事物的抽象级别。或者您可以发现无论您必须做什么都更适合其他结构或算法,然后您可以消除许多不必要的东西。
多年前,我记得有个同事总是编写数据库游标,做任何事情,因为那是他知道如何做的全部。设置和撤销游标需要一些样板,以及带有某些错误检查的循环,因此他的代码始终看起来表面上很复杂。他会在存储过程中执行它,这当然增加了更多的样板。

现在,正如所发生的那样,这是在Sybase T-SQL中完成的,它具有一个全局错误变量和一个光标状态全局变量,他会两次检查它们,一次当他得到第一个光标行时,然后在遍历所有其他行的循环中再检查一次。他经常混淆错误变量(@@error)和光标状态变量(@@sqlstatus,在成功时为零(获得光标行),失败时为1,在光标中没有更多行时为2)。他的代码在名义上的路径上可以工作,因为如果他已经得到了一行,则两者都为零,当他试图获取最后一行后的行时,他会得到一个错误。因此,如果你仔细看他的代码,你会叹气(再一次!),并替他修正。

但是,接下来你会更仔细地观察他的代码,并发现他正在像游标一样浏览x=1的所有行集,并且对于每一行设置y=y*2,你最终会告诉他:"只需编写一个更新语句!"。

你纠正他检查全局变量的错误虽然是正确的,但并没有解决实际问题,即他没有充分理由使用游标和存储过程,而只需从客户端代码发送一个更新语句。

找出这个问题更加困难,因为你不仅需要查看全局变量的本地使用情况,还需要查看两个地方:光标声明的位置(declare cursor_foo cursor for select * from table where x = 1 for update;)以及20行之后更新发生的位置(update table set y = y * 2 where current of cursor_foo)。(所有这些都将是多行的,并且非常模板化。)

确定问题所在更加困难,因为你假设没有人会花费这么大的力气来进行一次更新;当然所有这些样板代码必须是因为更新中发生了特殊的事情,对吗?还是因为where谓词是动态的或其他原因?因此,你会看到这个,作为一个尊重同事的谦虚程序员,你的第一反应会是,“我在这里错过了什么,肯定有一个使用光标的理由?”

(尽管我和他的老板都解释了@@error和@@sqlstatus之间的区别,但他从来没有真正理解,更不用说他几乎总是可以只进行一个update了;他以过程化的代码思考,从未“懂得”数据库,尽管自称为“数据库程序员”。)

教训是在确认需要细节(实现细节)之前不要陷入细节中。首先分解你的代码,你就有更好的机会理解你所处理的抽象,并真正改进代码。


4

目前的代码是否有充足的单元测试,以便您知道您的更改是否会破坏某些东西?如果没有,请在进行任何重构之前先编写它们。没有单元测试进行重构有点像走出街道而不首先看一眼。

如果代码已经拥有单元测试,您可以依靠它们来理解代码。否则,您需要充分了解代码以编写单元测试。这可能并不意味着您需要深入理解细枝末节。但是您将不得不理解各种不同条件下的预期结果。您需要知道代码的正常执行路径。您需要了解各种备选和错误路径。

我真的看不出什么情况下您不会对这个代码或至少其背后的意图变得非常熟悉。一旦达到那个水平,希望您能够不仅替换代码片段,而且实际设计一个更好的解决方案。

祝你好运。


没有单元测试的重构有点像没有先看路就走上街头。不幸的是,很多没有单元测试的代码已经处于需要进行重大重构的状态,才能够进行单元测试。:-/ - bignose

3

在开始修改代码之前,我会创建一个单元测试框架来确保不会破坏任何东西。然后,我逐一将所有情况移动到单独的函数中。由于我尝试给这些函数起易于理解的名称,因此代码突然变得更加易读。这也迫使我理解代码。完成此步骤后,我开始查看条件语句,看看它们是否可以进行重构,通常情况下是可能的,因为以前的大量代码可能会掩盖事物。(例如,我曾经继承了一个基本上有500行if-then-else的函数)


0

确定你的分支是基于“表格”还是功能。

某些事情(例如解析器)归结为本质上是一个大开关语句(稍微调整稀疏性),在这种情况下,一个巨大的开关语句可能是最可读的选项。

否则,请尝试以功能方式思考。一旦你将其分解成函数调用,你就有了重新排列事物的构建块,但你可能会发现这已经不再必要了。这是因为你选择的函数名称应遵循“只做一件事”的原则,所以当你重构到这些函数时,你已经在思考了。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接