language agnostic - Estilo de programación: ¿debe regresar temprano si no se cumple una condición de guardia?
language-agnostic control-flow (12)
Una cosa que a veces me he preguntado es ¿cuál es el mejor estilo de los dos que se muestran a continuación (si corresponde)? ¿Es mejor regresar inmediatamente si no se ha cumplido una condición de guardia, o solo debe hacer las otras cosas si se cumple la condición de guardia?
En aras de la argumentación, suponga que la condición de guardia es una prueba simple que devuelve un valor booleano, como verificar si un elemento está en una colección, en lugar de algo que pueda afectar el flujo de control lanzando una excepción. También asuma que los métodos / funciones son lo suficientemente cortos como para no requerir el desplazamiento del editor.
// Style 1
public SomeType aMethod() {
SomeType result = null;
if (!guardCondition()) {
return result;
}
doStuffToResult(result);
doMoreStuffToResult(result);
return result;
}
// Style 2
public SomeType aMethod() {
SomeType result = null;
if (guardCondition()) {
doStuffToResult(result);
doMoreStuffToResult(result);
}
return result;
}
A veces depende del idioma y de los tipos de "recursos" que esté utilizando (por ejemplo, archivos abiertos maneja).
En C, Style 2 es definitivamente más seguro y más conveniente porque una función tiene que cerrar y / o liberar los recursos que obtuvo durante la ejecución. Esto incluye los bloques de memoria asignados, los manejadores de archivos, los manejadores de los recursos del sistema operativo, como los hilos o los contextos de dibujo, los bloqueos en mutexes, y cualquier cantidad de cosas. Retrasar el return
hasta el final o restringir el número de salidas de una función permite al programador asegurarse más fácilmente de que él / ella limpia adecuadamente, lo que ayuda a evitar fugas de memoria, manejar fugas, interbloqueo y otros problemas.
En C ++ con programación estilo RAII , ambos estilos son igualmente seguros, por lo que puede elegir uno que sea más conveniente. Personalmente utilizo Style 1 con C ++ de estilo RAII. C ++ sin RAII es como C, entonces, de nuevo, el Estilo 2 es probablemente mejor en ese caso.
En idiomas como Java con recolección de basura, el tiempo de ejecución ayuda a suavizar las diferencias entre los dos estilos porque se limpia después de sí mismo. Sin embargo, también puede haber problemas sutiles con estos lenguajes si no "cierra" explícitamente algunos tipos de objetos. Por ejemplo, si construye un nuevo java.io.FileOutputStream
y no lo cierra antes de regresar, el identificador del sistema operativo asociado permanecerá abierto hasta que la basura del tiempo de ejecución recolecte la instancia de FileOutputStream
que ha quedado fuera del alcance. Esto podría significar que otro proceso o subproceso que necesite abrir el archivo para escribir no pueda hasta que se recopile la instancia de FileOutputStream
.
Aunque va en contra de las mejores prácticas que me han enseñado, me resulta mucho mejor reducir el anidamiento de las declaraciones if cuando tengo una condición como esta. Creo que es mucho más fácil de leer y, aunque sale en más de un lugar, es muy fácil de depurar.
El estilo 1 es lo que el kernel de Linux recomienda de manera indirecta.
De http://www.kernel.org/doc/Documentation/CodingStyle , capítulo 1:
Ahora, algunas personas afirmarán que tener sangrías de 8 caracteres hace que el código se mueva demasiado hacia la derecha y hace que sea difícil leer en una pantalla de terminal de 80 caracteres. La respuesta a eso es que si necesita más de 3 niveles de sangrado, de todas maneras está jodido y debería arreglar su programa.
El Estilo 2 agrega niveles de indentación, por lo tanto, se desaconseja.
Personalmente, también me gusta el estilo 1. El Estilo 2 hace que sea más difícil hacer coincidir las llaves de cierre en funciones que tienen varias pruebas de protección.
El número 1 es típicamente el camino fácil, flojo y descuidado. El número 2 expresa la lógica limpiamente. Lo que otros han señalado es que sí, puede volverse engorroso. Sin embargo, esta tendencia tiene un beneficio importante. El Estilo # 1 puede ocultar que tu función probablemente esté haciendo demasiado. No demuestra visualmente la complejidad de lo que está sucediendo muy bien. Es decir, evita que el código te diga "hey esto se está volviendo demasiado complejo para esta función". También hace que sea más fácil para otros desarrolladores que no conocen su código perderse esos retornos salpicados aquí y allá, a primera vista de todos modos.
Así que deja que el código hable. Cuando vea aparecer condiciones largas o anidadas si las declaraciones dicen que tal vez sería mejor dividir esto en varias funciones o que deba ser reescrito más elegantemente.
Habiendo sido entrenado en la Programación Estructurada de Jackson a finales de los 80, mi filosofía arraigada era siempre "una función debería tener un único punto de entrada y un único punto de salida"; esto significa que escribí el código de acuerdo con el Estilo 2.
En los últimos años me he dado cuenta de que el código escrito en este estilo a menudo es demasiado complejo y difícil de leer / mantener, y he cambiado al Estilo 1.
¿Quién dice que los perros viejos no pueden aprender nuevos trucos? ;)
No sé si la guardia es la palabra correcta aquí. Normalmente, un guardia insatisfecho genera una excepción o afirmación.
Pero además de esto , elegiría el estilo 1 porque, en mi opinión, mantiene el código más limpio. Tienes un ejemplo simple con una sola condición. Pero, ¿qué sucede con muchas condiciones y estilo 2? Conduce a una gran cantidad de condiciones anidadas if
o enormes if (con ||
, &&
). Creo que es mejor regresar de un método tan pronto como sepa que puede hacerlo.
Pero esto es ciertamente muy subjetivo ^^
Prefiero el primer estilo, excepto que no crearía una variable cuando no sea necesario. Yo haría esto:
// Style 3
public SomeType aMethod() {
if (!guardCondition()) {
return null;
}
SomeType result = new SomeType();
doStuffToResult(result);
doMoreStuffToResult(result);
return result;
}
Prefiero usar el método n. ° 1, es lógicamente más fácil de leer y lógicamente más similar a lo que intentamos hacer. (si sucede algo malo, salga de la función AHORA, no pase, no recoja $ 200)
Además, la mayoría de las veces desearía devolver un valor que no sea un resultado lógicamente posible (es decir, -1) para indicar al usuario que llamó a la función que la función no se ejecutó correctamente y para tomar las medidas apropiadas. Esto se presta mejor al método n. ° 1 también.
Si busca en .net-Framework usando .net-Reflector verá que los programadores de .net usan el estilo 1 (o tal vez el estilo 3 ya mencionado por unbeli). Las razones ya se mencionan en las respuestas anteriores. y tal vez otra razón es hacer que el código sea mejor legible, conciso y claro. Lo más que se usa este estilo es cuando se verifican los parámetros de entrada, siempre hay que hacer esto si se programa un tipo de frawework / library / dll. primero verifique todos los parámetros de entrada que trabaje con ellos.
Diría que Style1 se volvió más utilizado porque es la mejor práctica si la combinas con métodos pequeños.
Style2 parece una mejor solución cuando tienes grandes métodos. Cuando los tiene ... tiene un código común que quiere ejecutar sin importar cómo salga. Pero la solución adecuada no es forzar un solo punto de salida sino hacer que los métodos sean más pequeños.
Por ejemplo, si desea extraer una secuencia de código de un método grande, y este método tiene dos puntos de salida donde comienza a tener problemas, es difícil hacerlo automáticamente. Cuando tengo un gran método escrito en style1 por lo general lo transformo en style2, luego extraigo métodos y en cada uno de ellos debería tener el código Style1.
Entonces Style1 es mejor pero es compatible con métodos pequeños. Style2 no es tan bueno, pero se recomienda si tienes métodos grandes que no quieres y tienes tiempo para dividirte.
Yo diría "Depende de ..."
En situaciones donde tengo que realizar una secuencia de limpieza con más de 2 o 3 líneas antes de abandonar una función / método, preferiría el estilo 2 porque la secuencia de limpieza debe escribirse y modificarse solo una vez. Eso significa que la facilidad de mantenimiento es más fácil.
En todos los demás casos, preferiría el estilo 1.
Martin Fowler se refiere a esta refactorización como: "Reemplazar condicional anidado con cláusulas de guardia"
Si / else declaraciones también trae complejidad ciclomática. De ahí que sea más difícil probar casos. Para probar todos los bloques if / else, es posible que deba ingresar muchas opciones.
Donde como si hubiera alguna cláusula de guardia, puedes probarla primero, y tratar la lógica real dentro de las cláusulas if / else de una manera más clara.