具有验证检查的控制流样式


27

我发现自己在写很多这样的代码:

int myFunction(Person* person) {
  int personIsValid = !(person==NULL);
  if (personIsValid) {
     // do some stuff; might be lengthy
     int myresult = whatever;
     return myResult;
  }
  else {
    return -1;
  }
}

它可能会变得非常混乱,特别是如果涉及多个检查。在这种情况下,我尝试了多种样式,例如:

int netWorth(Person* person) {
  if (Person==NULL) {
    return -1;
  }
  if (!(person->isAlive))  {
    return -1;
  }
  int assets = person->assets;
  if (assets==-1)  {
    return -1;
  }
  int liabilities = person->liabilities;
  if (liabilities==-1) {
    return -1;
  }
  return assets - liabilities;
}

我对这里的样式选择感兴趣。[不要过于担心个别陈述的细节;使我感兴趣的是整体控制流程。]


8
请允许我指出您的示例中存在相当严重的规范错误。例如,如果资产== 42,负债== 43,则您将宣布此人不存在。
约翰·R·斯特罗姆

不会抛出异常并让客户端代码管理验证更好吗?
图兰斯·科尔多瓦

@TulainsCórdova异常可能不可用,或者无效数据不够特殊,以至于不能接受构建堆栈跟踪等的性能影响。
绿巨人

Answers:


27

对于此类问题,Martin Fowler提出了规范模式

...设计模式,通过使用布尔逻辑将业务规则链接在一起,可以重新组合业务规则。
 
规范模式概述了可与其他业务规则组合的业务规则。在这种模式下,业务逻辑单元从抽象的聚合Composite Specification类继承其功能。Composite Specification类具有一个称为IsSatisfiedBy的函数,该函数返回布尔值。实例化后,该规范与其他规范“链接”在一起,使新规范易于维护,但高度可自定义的业务逻辑。此外,在实例化时,业务逻辑可以通过方法调用或控制反转来更改其状态,以成为其他类(例如持久性存储库)的委托...

上面的声音听起来有些粗俗(至少对我来说),但是当我在代码中尝试时,它运行得很顺利,并且易于实现和阅读。

我的看法是,主要思想是将提取检查的代码“提取”到专用方法/对象中。

以您的netWorth示例为例,它可能如下所示:

int netWorth(Person* person) {
  if (isSatisfiedBySpec(person)) {
    return person->assets - person->liabilities;
  }
  log("person doesn't satisfy spec");
  return -1;
}

#define BOOLEAN int // assuming C here
BOOLEAN isSatisfiedBySpec(Person* person) {
  return Person != NULL
      && person->isAlive
      && person->assets != -1
      && person->liabilities != -1;
}

您的情况看起来很简单,因此所有检查看起来都可以,可以放在一个方法中的简单列表中。我经常不得不使用更多的方法来使它读起来更好。

我通常还会在专用对象中分组/提取与“ spec”相关的方法,尽管没有这种情况您的情况看起来还可以。

  // ...
  Specification s, *spec = initialize(s, person);
  if (spec->isSatisfied()) {
    return person->assets - person->liabilities;
  }
  log("person doesn't satisfy spec");
  return -1;
  // ...

除了上面提到的一个以外,Stack Overflow上的这个问题还建议了一些链接: 规范模式示例。尤其是,答案建议使用Dimecasts的“学习规范模式”作为示例的示例,并提及Eric Evans和Martin Fowler撰写的“ Specifications”论文


8

我发现将验证移至其自身的功能较为容易,它有助于保持其他功能的意图更加清晰,因此您的示例将如下所示。

int netWorth(Person* person) { 
    if(validPerson(person)) {
        int assets = person->assets;
        int liabilities = person->liabilities;
        return assets - liabilities;
    }
    else {
        return -1;
    }
}

bool validPerson(Person* person) { 
    if(person!=NULL && person->isAlive
      && person->assets !=-1 && person->liabilities != -1)
        return true;
    else
        return false;
}

2
你为什么要if加入validPerson?只需返回即可person!=NULL && person->isAlive && person->assets !=-1 && person->liabilities != -1
David Hammen '18

3

我看到特别有效的一件事是在代码中引入了一个验证层。首先,您有一个方法可以进行所有混乱的验证,并-1在出现问题时返回错误(如上述示例中所示)。验证完成后,该函数将调用另一个函数进行实际工作。现在,此功能不需要执行所有这些验证步骤,因为它们应该已经完成​​。也就是说,功函数假定输入有效。您应该如何应对假设?您在代码中声明它们。

我认为这使代码非常易于阅读。验证方法包含所有混乱的代码,以处理用户端的错误。工作方法使用断言明确地记录了其假设,然后不必使用可能无效的数据。

考虑一下示例的重构:

int myFunction(Person* person) {
  int personIsValid = !(person==NULL);
  if (personIsValid) {
     return myFunctionWork(person)
  }
  else {
    return -1;
  }
}

int myFunction(Person *person) {
  assert( person != NULL);  
  // Do work and return
}
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.