使用条件句是否是反模式?


14

我在工作中的遗留系统中看到了很多东西-这些功能如下所示:

bool todo = false;
if(cond1)
{
  ... // lots of code here
  if(cond2)
    todo = true;
  ... // some other code here
}

if(todo)
{
  ...
}

换句话说,功能有两个部分。第一部分进行某种处理(可能包含循环,副作用等),并沿途设置“ todo”标志。仅当已设置“ todo”标志时才执行第二部分。

这似乎是一种非常丑陋的处理方式,而且我认为我花了很多时间去理解的大多数情况都可以重构,以避免使用该标志。但这是一个实际的反模式,一个坏主意还是完全可以接受?

第一个明显的重构是将其分为两种方法。但是,我的问题更多是关于是否有必要(在现代的OO语言中)创建一个局部标志变量,可能将其设置在多个位置,然后在以后使用它来决定是否执行下一个代码块。


2
您如何重构它?
陶Szelei

13
假设根据几个非平凡的非排他性条件在多个地方设置了待办事项,那么我很难想到一个重构有一点点意义。如果没有重构,则没有反模式。除了todo变量的命名;应该命名为更具表现力的名称,例如“ doSecurityCheck”。
user281377 2011年

3
@ammoQ:+1;如果事情很复杂,那就是这样。在某些情况下,标志变量可能更有意义,因为它可以更清楚地做出决定,并且您可以搜索它以找到做出决定的位置。
Donal Fellows,

1
@Donal研究员:如果需要搜索原因,我将变量设为列表;只要为空,就为“ false”;无论设置了哪个标志,都会将原因代码添加到列表中。因此,您可能会得到一个类似的列表["blacklisted-domain","suspicious-characters","too-long"],表明存在多种原因。
user281377 2011年

2
我不认为这是一种反模式,但绝对是一种气味
Binary Worrier

Answers:


23

我不了解反模式,但是我从中提取了三种方法。

第一个将执行一些工作并返回一个布尔值。

第二种将执行“其他代码”执行的任何工作

如果返回的布尔值为true,则第三个将执行辅助工作。

如果很重要的一点是,如果第一个方法返回true,则仅第二个(并且总是)被调用很重要,那么提取的方法可能是私有的。

通过很好地命名方法,我希望它可以使代码更清晰。

像这样:

public void originalMethod() {
    bool furtherProcessingRequired = lotsOfCode();
    someOtherCode();
    if (furtherProcessingRequired) {
        doFurtherProcessing();
    }
    return;
}

private boolean lotsOfCode() {
    if (cond1) {
        ... // lots of code here
        if(cond2) {
            return true;
        }
    }
    return false;
}

private void someOtherCode() {
    ... // some other code here
}

private void doFurtherProcessing() {
    // Do whatever is needed
}

显然,关于早期返回是否可以接受尚有争议,但这是一个实现细节(代码格式化标准也是如此)。

关键是代码的意图变得更加清晰,这很好。

关于这个问题的评论之一表明,这种模式代表一种气味,我同意这一点。值得一看,看看是否可以使意图更清晰。


拆分为2个函数仍将需要一个todo变量,并且可能很难理解。
2011年

是的,我也会这样做,但是我的问题更多是关于“ todo”标志的用法。
rick 2011年

2
如果最后得到if (check_if_needed ()) do_whatever ();,那里没有明显的标志。如果代码相当简单,我认为这可能会使代码分解过多,并可能损害可读性。毕竟,您所做的工作的详细信息do_whatever可能会影响您的测试方式check_if_needed,因此将所有代码放在同一屏幕上很有用。另外,这不能保证check_if_needed可以避免使用标志-如果这样做,它可能会使用多个return语句来执行它,这可能会破坏严格的一次退出拥护者。
Steve314,2011年

3
@ Pubby8他说“从中提取2种方法”,结果得到3种方法。2种方法进行实际处理,并与原始方法协调工作流程。这将是一个更清洁的设计。
MattDavey 2011年

这忽略了... // some other code here早期归还案件
Caleth

6

我认为这种丑陋是由于以下事实:单个方法中有很多代码,和/或变量的名称不正确(它们本身都是代码的味道 -反模式是IMO的抽象和复杂性)。

因此,如果按照@Bill的建议将大多数代码提取到较低级的方法中,其余的代码将变得干净(至少对我而言)。例如

bool registrationNeeded = installSoftware(...);
if (registrationNeeded) {
  registerUser(...)
}

或者,您甚至可以通过将标志检查隐藏到第二种方法中并使用如下形式来完全摆脱本地标志

calculateTaxRefund(isTaxRefundable(...), ...)

总体而言,我认为本地标志变量本身不一定很糟糕。上面的哪个选项更易读(对我而言更可取)取决于方法参数的数量,选择的名称以及哪种形式与代码的内部逻辑更一致。


4

todo对于变量来说确实是个坏名字,但是我认为这可能是错误的。没有上下文很难完全确定。

假设函数的第二部分对列表进行排序,该列表由第一部分构建。这应该更具可读性:

bool requiresSorting = false;
if(cond1)
{
    ... // lots of code here
    if(cond2)
        requiresSorting = true;
    ... // some other code here
}

if(requiresSorting)
{
    ...
}

但是,比尔的建议也是正确的。这仍然更具可读性:

bool requiresSorting = BuildList(list);
if (requiresSorting)
    SortList(list);

为什么不更进一步:if(BuildList(list))SortList(list);
Phil N DeBlanc

2

在我看来,状态机模式很好。其中的反模式是“ todo”(坏名)和“很多代码”。


我敢肯定那只是为了说明。
罗伦·佩希特尔

1
同意 我要传达的是,不应将代码质量低下的好模式淹没在不良代码中。
ptyx 2011年

1

这真的取决于。如果由todo(由我希望您不要真正使用该名称来保护它的代码,因为它完全没有助记符!)从概念上讲是清理代码,则您已获得反模式,应使用C ++的RAII或C#的代码using构造处理事物。

另一方面,如果从概念上讲不是清理阶段,而只是有时需要进行的其他处理,并且需要在较早的时候做出决定,那么书面内容就可以了。当然,请考虑是否可以将单个代码块更好地重构为它们自己的功能,以及是否已经在其名称中捕获了flag变量的含义,但是这种基本代码模式是可以的。特别是,尝试在其他函数中投入过多的内容可能会使所发生的事情变得不太清楚,肯定是一种反模式。


显然,这不是清除操作-它并不总是运行。我以前遇到过这样的情况-在处理某些事情时,您可能最终会使某种预先计算的结果无效。如果计算成本很高,则只需要在需要时运行它。
洛伦·佩希特尔

1

这里的许多答案在通过复杂性检查时都会遇到麻烦,有些看起来> 10。

我认为这是您正在查看的“反模式”部分。找到一种可以测量代码圈复杂度的工具-有用于eclipse的插件。本质上,它是对代码测试难度的一种衡量,涉及代码分支的数量和级别。

总的来说,可能的解决方案是,您的代码布局使我想到“任务”,如果这发生在很多地方,也许您真正想要的是面向任务的体系结构-每个任务都是自己的对象,在中间任务中,您可以通过实例化另一个任务对象并将其扔到队列中来排队下一个任务。这些代码的设置非常简单,而且可以显着降低某些类型的代码的复杂性,但是正如我所说的那样,这完全是一头雾水。


1

使用上面的pdr示例,因为它是一个很好的示例,所以我将进一步走下去。

他有:

bool requiresSorting = BuildList(list);
if (requiresSorting)
    SortList(list);

因此,我意识到以下方法会起作用:

if(BuildList(list)) 
    SortList(list)

但是还不清楚。

因此,对于原始问题,为什么没有:

BuildList(list)
SortList(list)

然后让SortList决定是否需要排序?

您会看到您的BuildList方法似乎了解排序,因为它会返回一个布尔值指示,但是对于设计用于构建列表的方法而言,这毫无意义。


当然,下一步是问为什么这是一个两步过程。在任何我看到这样的代码的地方,我都重构为一个名为BuildAndSortList(list)的方法
Ian

这不是答案。您更改了代码的行为。
D Drmmr

并不是的。再次,我不敢相信我要回复我7年前发布的内容,但是到底是什么:)我在争论的是SortList将包含条件。如果您有一个单元测试断言该列表仅在满足条件x时进行排序,则该列表仍会通过。通过将条件移到SortList中,可以避免总是写(if(something)然后SortList(...))
Ian

0

是的,这似乎是一个问题,因为您必须不断跟踪标记标记为ON / OFF的所有位置。最好将逻辑作为嵌套if条件包含在内部,而不是将逻辑删除。

同样是丰富的领域模型,在这种情况下,只有一个衬里将在对象内部做大事。


0

如果仅将标志设置一次,则将
...
代码直接移至
... //其他一些代码
之后,然后取消该标志。

否则,将
// //这里的大量代码
... // 这里的其他一些代码
...
如果可能的话,将代码分成单独的函数,因此很明显,此函数负有责任,即分支逻辑。


... //这里的大量代码尽可能
地分成两个或多个函数,其中一些完成某些工作(这是一个命令),而其他一些则返回todo值(这是一个查询)或使之工作非常明确地他们正在修改它(使用副作用的查询)

代码本身不是在这里发生的反模式...我怀疑将分支逻辑与实际工作(命令)混合在一起是您要寻找的反模式。


这篇文章补充了哪些内容缺少现有答案?
esoterik

@esoterik有时,在涉及标志时,常常会忽略添加少量CQRS的机会……决定更改标志的逻辑表示查询,而执行工作表示命令。有时将两者分开可以使代码更清晰。同样值得在上面的代码中指出它可以简化,因为该标志仅在一个分支中设置。我认为标志不是反模式,如果标志的名称实际上使代码更具表现力,那么这是一件好事。我觉得如果可能的话,在代码中创建,设置和使用标志的位置应该靠近。
安德鲁·帕特

0

有时我发现我需要实现这种模式。有时您想要在执行操作之前执行多次检查。出于效率考虑,除非确实有必要进行检查,否则不进行涉及某些检查的计算。因此,您通常会看到类似以下的代码:

// Check individual fields for proper input

if(fieldsValidated) {
  // Perform cross-checks to see if input contains values which exist in the database

  if(valuesExist) {
    try {
      // Attempt insertion
      trx.commit();
    } catch (DatabaseException dbe) {
      trx.rollback();
      throw dbe;
    }
  } else {
    closeConnection(db);
    throwException();
  }
} else {
  closeConnection(db);
  throwException();
}

通过将验证与执行操作的实际过程分开,可以简化此过程,因此您会看到更多类似的内容:

boolean proceed = true;
// Check individual fields for proper input

if(fieldsValidated) {
  // Perform cross-checks to see if input contains values which exist in the database

  if(!valuesExist) {
    proceed = false;
  }
} else {
  proceed = false;
}

// The moment of truth
if(proceed) {
  try {
    // Attempt insertion
    trx.commit();
  } catch (DatabaseException dbe) {
    trx.rollback();
    throw dbe;
  }
} else {
  if(db.isOpen()) {
    closeConnection(db);
  }
  throwException();
}

显然,它会根据您要实现的目标而有所不同,尽管是这样编写的,但是“成功”代码和“失败”代码都只编写了一次,从而简化了逻辑并保持了相同的性能水平。从那里开始,将整个验证级别放入内部方法中是一个好主意,该内部方法返回布尔值指示成功或失败,这进一步简化了事情,尽管有些程序员出于某些奇怪的原因而喜欢极长的方法。


在您给出的示例中,我想我希望有一个函数shouldIDoIt(fieldsValidated,valuesExist)返回答案。这是因为与我在工作中看到的代码相反,都是一次确定是/否的决定,将继续进行的决策分散到几个不同的非连续位置。
Kricket 2011年

@KelseyRider,这就是重点。将执行与验证分离开来,您可以将逻辑填充到方法中,以简化程序的总体逻辑,使其成为if(isValidated())doOperation()
Neil

0

这不是一个模式。最一般的解释是您要设置一个布尔变量,然后在其值上分支。那是正常的过程编程,仅此而已。

现在,您的特定示例可以重写为:

if(cond1)
{
    ... // lots of code here
    ... // some other code here
    if (cond2)
    {
        ...
    }
}

那可能更容易阅读。或者可能不是。这取决于您省略的其余代码。专注于使代码更简洁。


-1

用于控制流的本地标志应被识别为goto伪装形式。如果仅在函数中使用标志,则可以通过写出函数的两个副本,将其中一个标记为“ flag is true”,将另一个标记为“ flag is false”,并替换设置该标志的每个操作来消除它清除时或设置时清除,在函数的两个版本之间跳转。

在许多情况下,使用标志的代码将比使用标志的代码更干净goto,但这并不总是正确的。在某些情况下,使用goto跳过一段代码可能比使用标志这样做更干净(尽管有些人可能会在此处插入某些猛禽卡通)。

我认为主要指导原则应该是程序逻辑流程应尽可能类似于业务逻辑描述。如果根据以怪异方式拆分和合并的状态来描述业务逻辑要求,则使程序逻辑同样可以比尝试使用标志来隐藏这种逻辑更干净。另一方面,如果描述业务规则的最自然的方式是说如果已经执行了某些其他动作,则应该执行一个动作,那么最自然的表达方式可能是使用执行时设置的标志。后者的动作,否则就很清楚了。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.