Estilo para control de flujo con verificaciones de validación

27

Me encuentro escribiendo mucho código 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;
  }
}

Puede volverse bastante complicado, especialmente si hay varias comprobaciones involucradas. En tales casos, he experimentado con 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;
}

Me interesan los comentarios sobre las elecciones estilísticas aquí. [No se preocupe demasiado por los detalles de las declaraciones individuales; es el flujo de control general lo que me interesa.]

William Jockusch
fuente
8
Permítame señalar que tiene un error de especificación bastante grave en su ejemplo. Si, por ejemplo, activos == 42 y pasivos == 43, declarará que la persona no existe.
John R. Strohm
¿No sería mejor lanzar una excepción y dejar que el código del cliente administre las validaciones?
Tulains Córdova
@ TulainsCórdova Las excepciones pueden no estar disponibles, o tal vez los datos no válidos no sean lo suficientemente excepcionales como para que el impacto en el rendimiento de la construcción del seguimiento de la pila, etc. sea aceptable.
Hulk

Respuestas:

27

Para estos temas amables Martin Fowler propuso especificación del patrón :

... patrón de diseño, mediante el cual las reglas de negocio pueden recombinarse encadenando las reglas de negocio juntas usando lógica booleana.
 
Un patrón de especificación describe una regla comercial que se puede combinar con otras reglas comerciales. En este patrón, una unidad de lógica de negocios hereda su funcionalidad de la clase de Especificación compuesta agregada abstracta. La clase de especificación compuesta tiene una función llamada IsSatisfiedBy que devuelve un valor booleano. Después de la creación de instancias, la especificación se "encadena" con otras especificaciones, lo que hace que las nuevas especificaciones sean fáciles de mantener, pero con una lógica empresarial altamente personalizable. Además, tras la creación de instancias, la lógica de negocios puede, a través de la invocación de métodos o la inversión de control, alterar su estado para convertirse en un delegado de otras clases, como un repositorio de persistencia ...

Lo anterior suena un poco alto (al menos para mí), pero cuando lo probé en mi código, fue bastante sencillo y resultó fácil de implementar y leer.

A mi modo de ver, la idea principal es "extraer" el código que realiza las comprobaciones en métodos u objetos dedicados.

Con su netWorthejemplo, esto podría verse de la siguiente manera:

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

Su caso parece bastante simple para que todas las comprobaciones se vean bien y quepan en una lista simple dentro de un solo método. A menudo tengo que dividirme en más métodos para que se lea mejor.

También suelo agrupar / extraer métodos relacionados con "especificaciones" en un objeto dedicado, aunque su caso se ve bien sin eso.

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

Esta pregunta en Stack Overflow recomienda algunos enlaces además de uno mencionado anteriormente: Ejemplo de patrón de especificación . En particular, las respuestas sugieren Dimecasts 'Learning the Specification pattern' para un tutorial de un ejemplo y mencionan el documento "Especificaciones" escrito por Eric Evans y Martin Fowler .

mosquito
fuente
8

Me resulta más fácil mover la validación a su propia función, ayuda a mantener limpia la intención de otras funciones, por lo que su ejemplo sería así.

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
fuente
2
¿Por qué tiene el ifen validPerson? Simplemente regrese person!=NULL && person->isAlive && person->assets !=-1 && person->liabilities != -1en su lugar.
David Hammen
3

Una cosa que he visto funcionar particularmente bien es la introducción de una capa de validación en su código. Primero tiene un método que hace toda la validación desordenada y devuelve errores (como -1en sus ejemplos anteriores) cuando algo sale mal. Cuando se realiza la validación, la función llama a otra función para hacer el trabajo real. Ahora esta función no necesita hacer todos esos pasos de validación porque ya deberían estar hechos. Es decir, la función de trabajo supone que la entrada es válida. ¿Cómo debe lidiar con los supuestos? Los afirmas en el código.

Creo que esto hace que el código sea muy fácil de leer. El método de validación contiene todo el código desordenado para tratar los errores del lado del usuario. El método de trabajo documenta limpiamente sus supuestos con afirmaciones y luego no tiene que funcionar con datos potencialmente inválidos.

Considere esta refactorización de su ejemplo:

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
fuente