Estilo para fluxo de controle com verificações de validação

27

Eu me pego escrevendo muitos códigos como este:

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

Pode ficar bem confuso, principalmente se houver várias verificações. Nesses casos, experimentei estilos alternativos, como este:

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;
}

Estou interessado em comentários sobre as escolhas estilísticas aqui. [Não se preocupe muito com os detalhes das declarações individuais; é o fluxo geral de controle que me interessa.]

William Jockusch
fonte
8
Permitam-me salientar que você tem um erro de especificação bastante sério no seu exemplo. Se, por exemplo, ativos == 42 e passivos == 43, você declarará a pessoa inexistente.
John R. Strohm
Não seria possível lançar uma exceção e deixar o código do cliente gerenciar validações?
Tulains Córdova
As exceções do @ TulainsCórdova podem não estar disponíveis ou talvez os dados inválidos não sejam excepcionais o suficiente para que o impacto no desempenho da criação do rastreamento de pilha etc. seja aceitável.
Hulk

Respostas:

27

Para este tipo de questões, Martin Fowler propôs um padrão de especificação :

... padrão de design, no qual as regras de negócios podem ser recombinadas, encadeando as regras de negócios usando a lógica booleana.
 
Um padrão de especificação descreve uma regra de negócios que pode ser combinada com outras regras de negócios. Nesse padrão, uma unidade de lógica de negócios herda sua funcionalidade da classe agregada abstrata Composite Specification. A classe Composite Specification possui uma função chamada IsSatisfiedBy que retorna um valor booleano. Após a instanciação, a especificação é "encadeada" com outras especificações, tornando as novas especificações fáceis de manter, mas com lógica de negócios altamente personalizável. Além disso, por instanciação, a lógica de negócios pode, por meio de invocação de método ou inversão de controle, ter seu estado alterado para se tornar um delegado de outras classes, como um repositório de persistência ...

Acima parece um pouco exagerado (pelo menos para mim), mas quando eu tentei no meu código, ele foi bem tranquilo e ficou fácil de implementar e ler.

Na minha opinião, a idéia principal é "extrair" o código que faz as verificações nos métodos / objetos dedicados.

Com o seu netWorthexemplo, isso pode ter a seguinte aparência:

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;
}

Seu caso parece bastante simples, de modo que todas as verificações parecem OK para caber em uma lista simples em um único método. Muitas vezes, tenho que dividir para mais métodos para torná-lo melhor.

Eu também geralmente agrupo / extraio métodos relacionados "spec" em um objeto dedicado, embora seu caso pareça bom sem isso.

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

Esta pergunta no Stack Overflow recomenda alguns links além do mencionado acima: Exemplo de padrão de especificação . Em particular, as respostas sugerem o Dimecasts 'Learning the Specification pattern' para uma explicação passo a passo de um exemplo e mencionam o artigo "Specifications", de autoria de Eric Evans e Martin Fowler .

mosquito
fonte
8

Acho mais fácil mover a validação para sua própria função; isso ajuda a manter a intenção de outras funções mais limpas; portanto, seu exemplo seria assim.

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;
}
Ryathal
fonte
2
Por que você tem o ifin validPerson? Simplesmente retorne person!=NULL && person->isAlive && person->assets !=-1 && person->liabilities != -1.
David Hammen
3

Uma coisa que eu vi funcionar particularmente bem é a introdução de uma camada de validação no seu código. Primeiro, você tem um método que faz toda a validação confusa e retorna erros (como -1nos exemplos acima) quando algo dá errado. Quando a validação é concluída, a função chama outra função para fazer o trabalho real. Agora, essa função não precisa executar todas essas etapas de validação, porque elas já devem estar concluídas. Ou seja, a função de trabalho assume que a entrada é válida. Como você deve lidar com suposições? Você os afirma no código.

Eu acho que isso torna o código muito fácil de ler. O método de validação contém todo o código confuso para lidar com erros do lado do usuário. O método de trabalho documenta claramente suas suposições com afirmações e não precisa trabalhar com dados potencialmente inválidos.

Considere esta refatoração do seu exemplo:

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
}
Oleksi
fonte