Esta es una situación común y hay muchas formas comunes de tratarla. Aquí está mi intento de una respuesta canónica. Comenta si me perdí algo y mantendré esta publicación actualizada.
Esta es una flecha
Lo que está discutiendo se conoce como el antipatrón de flecha . Se llama flecha porque la cadena de ifs anidados forma bloques de código que se expanden más y más hacia la derecha y luego hacia la izquierda, formando una flecha visual que "apunta" al lado derecho del panel del editor de código.
Aplana la flecha con la guardia
Aquí se discuten algunas formas comunes de evitar la flecha . El método más común es usar un patrón de protección , en el que el código maneja los flujos de excepción primero y luego maneja el flujo básico, por ejemplo, en lugar de
if (ok)
{
DoSomething();
}
else
{
_log.Error("oops");
return;
}
... usarías ...
if (!ok)
{
_log.Error("oops");
return;
}
DoSomething(); //notice how this is already farther to the left than the example above
Cuando hay una larga serie de guardias, esto aplana el código considerablemente ya que todos los guardias aparecen completamente a la izquierda y sus ifs no están anidados. Además, está visualmente emparejando la condición lógica con su error asociado, lo que hace que sea mucho más fácil saber lo que está sucediendo:
Flecha:
ok = DoSomething1();
if (ok)
{
ok = DoSomething2();
if (ok)
{
ok = DoSomething3();
if (!ok)
{
_log.Error("oops"); //Tip of the Arrow
return;
}
}
else
{
_log.Error("oops");
return;
}
}
else
{
_log.Error("oops");
return;
}
Guardia:
ok = DoSomething1();
if (!ok)
{
_log.Error("oops");
return;
}
ok = DoSomething2();
if (!ok)
{
_log.Error("oops");
return;
}
ok = DoSomething3();
if (!ok)
{
_log.Error("oops");
return;
}
ok = DoSomething4();
if (!ok)
{
_log.Error("oops");
return;
}
Esto es objetiva y cuantificablemente más fácil de leer porque
- Los caracteres {y} para un bloque lógico dado están más juntos
- La cantidad de contexto mental necesaria para comprender una línea particular es menor
- La totalidad de la lógica asociada con una condición if es más probable que esté en una página
- La necesidad de que el codificador desplace la página / pista ocular disminuye considerablemente
Cómo agregar código común al final
El problema con el patrón de guardia es que se basa en lo que se llama "retorno oportunista" o "salida oportunista". En otras palabras, rompe el patrón de que todas y cada una de las funciones deben tener exactamente un punto de salida. Este es un problema por dos razones:
- A algunas personas les molesta, por ejemplo, las personas que aprendieron a codificar en Pascal han aprendido que una función = un punto de salida.
- No proporciona una sección de código que se ejecute al salir, pase lo que pase , cuál es el tema en cuestión.
A continuación, proporcioné algunas opciones para solucionar esta limitación mediante el uso de funciones de idioma o evitando el problema por completo.
Opción 1. No puede hacer esto: use finally
Desafortunadamente, como desarrollador de c ++, no puedes hacer esto. Pero esta es la respuesta número uno para los idiomas que contienen finalmente una palabra clave, ya que esto es exactamente para lo que sirve.
try
{
if (!ok)
{
_log.Error("oops");
return;
}
DoSomething(); //notice how this is already farther to the left than the example above
}
finally
{
DoSomethingNoMatterWhat();
}
Opción 2. Evita el problema: reestructura tus funciones
Puede evitar el problema dividiendo el código en dos funciones. Esta solución tiene la ventaja de funcionar para cualquier idioma y, además, puede reducir la complejidad ciclomática , que es una forma comprobada de reducir la tasa de defectos y mejora la especificidad de cualquier prueba unitaria automatizada.
Aquí hay un ejemplo:
void OuterFunction()
{
DoSomethingIfPossible();
DoSomethingNoMatterWhat();
}
void DoSomethingIfPossible()
{
if (!ok)
{
_log.Error("Oops");
return;
}
DoSomething();
}
Opción 3. Truco de idioma: usa un bucle falso
Otro truco común que veo es usar while (verdadero) y break, como se muestra en las otras respuestas.
while(true)
{
if (!ok) break;
DoSomething();
break; //important
}
DoSomethingNoMatterWhat();
Si bien esto es menos "honesto" que el uso goto
, es menos propenso a equivocarse al refactorizar, ya que marca claramente los límites del alcance lógico. ¡Un ingenuo programador que corta y pega sus etiquetas o sus goto
declaraciones puede causar grandes problemas! (Y, francamente, el patrón es tan común ahora que creo que comunica claramente la intención y, por lo tanto, no es "deshonesto" en absoluto).
Hay otras variantes de estas opciones. Por ejemplo, uno podría usar en switch
lugar de while
. Cualquier construcción de lenguaje con una break
palabra clave probablemente funcionaría.
Opción 4. Aproveche el ciclo de vida del objeto
Otro enfoque aprovecha el ciclo de vida del objeto. Use un objeto de contexto para transportar sus parámetros (algo de lo que nuestro ingenuo ejemplo carece sospechosamente) y deséchelo cuando haya terminado.
class MyContext
{
~MyContext()
{
DoSomethingNoMatterWhat();
}
}
void MainMethod()
{
MyContext myContext;
ok = DoSomething(myContext);
if (!ok)
{
_log.Error("Oops");
return;
}
ok = DoSomethingElse(myContext);
if (!ok)
{
_log.Error("Oops");
return;
}
ok = DoSomethingMore(myContext);
if (!ok)
{
_log.Error("Oops");
}
//DoSomethingNoMatterWhat will be called when myContext goes out of scope
}
Nota: Asegúrese de comprender el ciclo de vida del objeto de su idioma de elección. Necesitas algún tipo de recolección de basura determinista para que esto funcione, es decir, debes saber cuándo se llamará al destructor. En algunos idiomas deberá usar en Dispose
lugar de un destructor.
Opción 4.1. Aproveche el ciclo de vida del objeto (patrón de envoltura)
Si va a utilizar un enfoque orientado a objetos, puede hacerlo bien. Esta opción utiliza una clase para "ajustar" los recursos que requieren limpieza, así como sus otras operaciones.
class MyWrapper
{
bool DoSomething() {...};
bool DoSomethingElse() {...}
void ~MyWapper()
{
DoSomethingNoMatterWhat();
}
}
void MainMethod()
{
bool ok = myWrapper.DoSomething();
if (!ok)
_log.Error("Oops");
return;
}
ok = myWrapper.DoSomethingElse();
if (!ok)
_log.Error("Oops");
return;
}
}
//DoSomethingNoMatterWhat will be called when myWrapper is destroyed
Nuevamente, asegúrese de comprender el ciclo de vida de su objeto.
Opción 5. Truco del lenguaje: usar evaluación de cortocircuito
Otra técnica es aprovechar la evaluación de cortocircuito .
if (DoSomething1() && DoSomething2() && DoSomething3())
{
DoSomething4();
}
DoSomethingNoMatterWhat();
Esta solución aprovecha la forma en que funciona el operador &&. Cuando el lado izquierdo de && se evalúa como falso, el lado derecho nunca se evalúa.
Este truco es más útil cuando se requiere un código compacto y cuando no es probable que el código vea mucho mantenimiento, por ejemplo, está implementando un algoritmo bien conocido. Para una codificación más general, la estructura de este código es demasiado frágil; Incluso un cambio menor en la lógica podría desencadenar una reescritura total.