Esta es una queja menor, pero cada vez que tengo que codificar algo como esto, la repetición me molesta, pero no estoy seguro de que ninguna de las soluciones sea peor.
if(FileExists(file))
{
contents = OpenFile(file); // <-- prevents inclusion in if
if(SomeTest(contents))
{
DoSomething(contents);
}
else
{
DefaultAction();
}
}
else
{
DefaultAction();
}
- ¿Hay un nombre para este tipo de lógica?
- ¿Soy un poco demasiado TOC?
Estoy abierto a sugerencias de códigos malvados, aunque solo sea por curiosidad ...
coding-style
conditions
Benjol
fuente
fuente
DefaultAction
llamadas violan el principio DRYmake sure I have valid data for DoSomething(), and then DoSomething() with it. Otherwise, take DefaultAction()
. Los detalles esenciales de asegurarse de tener los datos para DoSomething () están en un nivel de abstracción más bajo y, por lo tanto, deberían estar en una función diferente. Esta función tendrá un nombre en el nivel superior de abstracción, y su implementación será de bajo nivel. Las buenas respuestas a continuación abordan este problema.Respuestas:
Extraerlo para separar la función (método) y usar la
return
declaración:O, tal vez mejor, separe los contenidos y su procesamiento:
Upd:
Por qué no excepciones, por qué
OpenFile
no arroja una excepción IO:creo que es una pregunta realmente genérica, en lugar de una pregunta sobre el archivo IO. Nombres como
FileExists
,OpenFile
puede ser confuso, pero si para sustituirlos porFoo
,Bar
, etc, - Sería más claro queDefaultAction
se puede llamar las veces que seanDoSomething
, por lo que puede ser el caso no excepcional. Péter Török escribió sobre esto al final de su respuesta.Por qué hay un operador condicional ternario en la segunda variante:
si hubiera una etiqueta [C ++], escribiría una
if
declaración con declaración decontents
en su parte de condición:Pero para otros lenguajes (tipo C),
if(contents) ...; else ...;
es exactamente lo mismo que la declaración de expresión con operador condicional ternario, pero más larga. Debido a que la parte principal del código era laget_contents
función, simplemente utilicé la versión más corta (y también omití elcontents
tipo). De todos modos, está más allá de esta pregunta.fuente
Si el lenguaje de programación que está utilizando (0) hace cortocircuitos en las comparaciones binarias (es decir, si no llama
SomeTest
siFileExists
devuelve falso) y (1) la asignación devuelve un valor (el resultado deOpenFile
se asignacontents
y luego ese valor se pasa como argumento aSomeTest
), se puede usar algo como lo siguiente, pero aún se le recomienda que le permite comentar el código señalar que la única=
es intencional.Dependiendo de cuán complicado sea el if, podría ser mejor tener una variable de bandera (que separa la prueba de las condiciones de éxito / falla con el código que maneja el error
DefaultAction
en este caso)fuente
if
declaración, en mi opinión.Más en serio que la repetición de la llamada a DefaultAction es el estilo en sí mismo porque el código está escrito no ortogonal (vea esta respuesta por buenas razones para escribir ortogonalmente).
Para mostrar por qué el código no ortogonal es malo, considere el ejemplo original, cuando se introduce un nuevo requisito de que no debemos abrir el archivo si está almacenado en un disco de red. Bueno, entonces podríamos actualizar el código a lo siguiente:
Pero también viene el requisito de que tampoco debemos abrir archivos grandes de más de 2 Gb. Bueno, acabamos de actualizar nuevamente:
Debe quedar muy claro que ese estilo de código será un gran problema de mantenimiento.
Entre las respuestas aquí que están escritas correctamente ortogonalmente están el segundo ejemplo de Abyx y la respuesta de Jan Hudec , por lo que no repetiré eso, solo señale que agregar los dos requisitos en esas respuestas sería simplemente
(o en
goto notexists;
lugar dereturn null;
), sin afectar a ningún otro código que las líneas agregadas . Por ejemplo, ortogonal.Al realizar la prueba, la regla general debe ser probar las excepciones, no el caso normal .
fuente
Obviamente:
Dijiste que estás abierto incluso a soluciones malvadas, así que usar el mal goto cuenta, no?
De hecho, dependiendo del contexto, esta solución podría ser menos malvada que el mal haciendo la acción dos veces o la variable extra malvada. Lo envolví en una función, porque definitivamente no estaría bien en el medio de una función larga (no menos importante debido al retorno en el medio). Pero que la función larga no está bien, punto.
Cuando tenga excepciones, serán más fáciles de leer, especialmente si puede tener OpenFile y DoSomething simplemente lanzar una excepción si no se cumplen las condiciones, por lo que no necesita verificaciones explícitas en absoluto. Por otro lado, en C ++, Java y C # lanzar una excepción es una operación lenta, por lo que desde el punto de rendimiento, el goto sigue siendo preferible.
Nota sobre "mal": la Pregunta frecuente C ++ 6.15 define "mal" como:
Y eso se aplica
goto
en este contexto. Las construcciones de control de flujo estructuradas son mejores la mayoría de las veces, pero cuando te encuentras en la situación en la que acumulan demasiados de sus propios males, como la asignación en condiciones, anidando más de aproximadamente 3 niveles de profundidad, duplicando el código o condiciones largas,goto
simplemente puede terminar siendo menos malvado.fuente
for(;;) { if(!FileExists(file)) break; contents = OpenFile(file); if(!SomeTest(contents)) break; DoSomething(contents); return; } /* broken out */ DefaultAction();
goto
, porque está abusandobreak
de una manera que nadie espera que se abuse, por lo que las personas que leen el código tendrán más problemas para ver hacia dónde los lleva la ruptura que con goto, que lo dice explícitamente. Además, está utilizando un bucle infinito que solo se ejecutará una vez, lo que será bastante confuso. Desafortunadamente,do { ... } while(0)
tampoco es exactamente legible, porque solo ves que es un bloque divertido cuando llegas al final y C no admite la ruptura de otros bloques (a diferencia de Perl)....
fuente
SomeTest
puede tener la misma semántica que la existencia del archivo siSomeTest
verifica el tipo de archivo, por ejemplo, comprueba que .gif es realmente un archivo GIF.contents && f(contents)
. ¿Dos funciones para salvar una más?Una posibilidad:
Por supuesto, esto hace que el código sea un poco más complejo de una manera diferente. Por lo tanto, es en gran medida una pregunta de estilo.
Un enfoque diferente sería utilizar excepciones, por ejemplo:
Esto parece más simple, sin embargo, solo es aplicable si
DefaultAction()
ajusta a cadaSomeTest()
tenga éxito, y un archivo faltante o una falla es claramente una condición errónea, por lo tanto, es adecuado lanzar una excepción sobre él.fuente
(function () { ... })()
en Javascript,{ flag = false; ... }
en C-like etc.Esto está en un nivel más alto de abstracción:
Y esto completa los detalles.
fuente
Algunas personas encuentran que este enfoque es un poco extremo, pero también es muy limpio. Permítame ilustrar en Python:
Cuando dice funciones deben hacer una cosa, se refiere a una cosa.
processFile()
elige una acción basada en el resultado de una prueba, y eso es todo lo que hace.fileMeetsTest()
combina todas las condiciones de la prueba, y eso es todo lo que hace.contentsTest()
transfiere la primera línea afirstLineTest()
, y eso es todo lo que hace.Parecen muchas funciones, pero se lee prácticamente como el inglés directo:
De acuerdo, eso es un poco prolijo, pero tenga en cuenta que si un mantenedor no se preocupa por los detalles, puede dejar de leer después de solo las 4 líneas de código
processFile()
, y aún tendrá un buen conocimiento de alto nivel de lo que hace la función.fuente
Con respecto a cómo se llama esto, puede convertirse fácilmente en el anti patrón de punta de flecha a medida que su código crece para manejar más requisitos (como se muestra en la respuesta proporcionada en https://softwareengineering.stackexchange.com/a/122625/33922 ) y luego cae en la trampa de tener grandes secciones de códigos con declaraciones condicionales anidadas que se asemejan a una flecha.
Ver enlaces como;
http://codinghorror.com/blog/2006/01/flattening-arrow-code.html
http://lostechies.com/chrismissal/2009/05/27/anti-patterns-and-worst-practices-the-arrowhead-anti-pattern/
Hay mucho más sobre este y otros patrones anti que se encuentran en Google.
Algunos buenos consejos que Jeff brinda en su blog con respecto a esto son;
Vea algunos de los comentarios en el blog de Jeff sobre las sugerencias de Steve McConnells sobre devoluciones anticipadas también;
Siempre me suscribí a la teoría de 1 entrada / salida por función debido a lo que me enseñaron hace 15 años más o menos. Creo que esto hace que el código sea mucho más fácil de leer y, como mencionas, más fácil de mantener
fuente
Esto se ajusta a las reglas DRY, no-goto y no-multiple-return, es escalable y legible en mi opinión:
fuente
if
dentro de otrosif
. Además, el código para manejar el caso fallido (DefaultAction()
) solo está en un lugar y para fines de depuración, el código no salta alrededor de las funciones auxiliares y al agregar puntos de interrupción a las líneas dondesuccess
se cambia la variable puede mostrar rápidamente qué pruebas pasaron (por encima de las activadas punto de interrupción) y cuáles no se han probado (a continuación).success
aok_so_far
:)Lo extraería a un método separado y luego:
que también permite
entonces posiblemente podría eliminar las
DefaultAction
llamadas y dejar la ejecuciónDefaultAction
de la llamada:También me gusta el enfoque de Jeanne Pindar .
fuente
Para este caso particular, la respuesta es bastante fácil ...
Hay una condición de carrera entre
FileExists
yOpenFile
: ¿qué sucede si se elimina el archivo?La única forma sensata de lidiar con este caso particular es omitir
FileExists
:Esto resuelve perfectamente este problema y hace que el código sea más limpio.
En general: intente repensar el problema e idee otra solución que evite el problema por completo.
fuente
Otra posibilidad si no le gusta ver demasiados es dejar el uso de else por completo y agregar una declaración de devolución adicional. Lo demás es algo superfluo a menos que requiera una lógica más compleja para determinar si hay más de dos posibilidades de acción.
Por lo tanto, su ejemplo podría convertirse en:
Personalmente, no me importa usar la cláusula else, ya que establece explícitamente cómo se supone que funciona la lógica y, por lo tanto, mejora la legibilidad de su código. Sin embargo, algunas herramientas de embellecimiento de código prefieren simplificar a una sola instrucción if para desalentar la lógica de anidamiento.
fuente
El caso que se muestra en el código de muestra generalmente se puede reducir a una sola
if
declaración. En muchos sistemas, la función de abrir archivo devolverá un valor no válido si el archivo aún no existe. A veces este es el comportamiento predeterminado; otras veces, debe especificarse mediante un argumento. Esto significa que laFileExists
prueba se puede descartar, lo que también puede ayudar con las condiciones de carrera resultantes de la eliminación del archivo entre la prueba de existencia y la apertura del archivo.Esto no aborda directamente el problema de la mezcla de niveles de abstracción, ya que evita completamente el problema de múltiples pruebas imposibles de encadenar, aunque eliminar la prueba de existencia de archivos no es incompatible con separar los niveles de abstracción. Suponiendo que los identificadores de archivos no válidos son equivalentes a "falsos" y los identificadores de archivos se cierran cuando salen del alcance:
fuente
Estoy de acuerdo con frozenkoi, sin embargo, para C # de todos modos, pensé que sería útil seguir la sintaxis de los métodos TryParse.
fuente
Su código es feo porque está haciendo demasiado en una sola función. Desea procesar el archivo o realizar la acción predeterminada, así que comience diciendo que:
Los programadores de Perl y Ruby escriben
processFile(file) || defaultAction()
Ahora ve a escribir ProcessFile:
fuente
Por supuesto, solo puede ir tan lejos en escenarios como estos, pero aquí hay un camino a seguir:
Es posible que desee filtros adicionales. Entonces haz esto:
Aunque esto también podría tener sentido:
¿Cual es el mejor? Eso depende del problema del mundo real que estés enfrentando.
Pero lo que hay que quitar es: puedes hacer mucho con la composición y el polimorfismo.
fuente
¿Qué hay de malo con lo obvio?
¿Me parece bastante estándar? Para ese tipo de gran procedimiento en el que tienen que suceder muchas cosas pequeñas, el fracaso de cualquiera de ellas evitaría esto último. Las excepciones lo hacen un poco más limpio si esa es una opción.
fuente
Me doy cuenta de que esta es una vieja pregunta, pero noté un patrón que no se ha mencionado; principalmente, estableciendo una variable para luego determinar el / los método / s que le gustaría llamar (fuera del if ... else ...).
Esto es solo como otro ángulo a tener en cuenta para facilitar el trabajo del código. También permite cuándo puede agregar otro método para llamar o cambiar el método apropiado que debe llamarse en ciertas situaciones.
En lugar de tener que reemplazar todas las menciones del método (y tal vez faltan algunos escenarios), todas se enumeran al final del bloque if ... else ... y son más fáciles de leer y modificar. Tiendo a usar esto cuando, por ejemplo, se pueden llamar varios métodos, pero dentro de los anidados si ... de lo contrario ... se puede llamar a un método en varias coincidencias.
Si establece una variable que define el estado, podría tener muchas opciones profundamente anidadas y actualizar el estado cuando algo debe realizarse (o no).
Esto podría usarse como en el ejemplo que se hace en la pregunta donde estamos verificando si 'DoSomething' ha ocurrido, y si no, realiza la acción predeterminada. O podría tener un estado para cada método al que desee llamar, establecer cuando corresponda, luego llamar al método aplicable fuera del if ... si no ...
Al final de las instrucciones anidadas if ... else ..., verifica el estado y actúa en consecuencia. Esto significa que solo necesita una sola mención de un método en lugar de todas las ubicaciones en las que se debe aplicar.
fuente
Para reducir el IF anidado:
1 / regreso temprano;
2 / expresión compuesta (consciente de cortocircuito)
Entonces, su ejemplo puede ser refactorizado así:
fuente
Vi muchos ejemplos con "return" que también uso, pero a veces quiero evitar crear nuevas funciones y usar un bucle en su lugar:
Si desea escribir menos líneas u odia los bucles infinitos como yo, puede cambiar el tipo de bucle a "do ... while (0)" y evitar el último "break".
fuente
¿Qué tal esta solución?
Supuse que OpenFile devuelve un puntero, pero esto podría funcionar también con el tipo de valor devuelto al especificar algún valor predeterminado no retornable (códigos de error o algo así).
Por supuesto, no espero alguna acción posible a través del método SomeTest en el puntero NULL (pero nunca se sabe), por lo que esto también podría verse como una comprobación adicional del puntero NULL para la llamada SomeTest (contenido).
fuente
Claramente, la solución más elegante y concisa es usar una macro de preprocesador.
Lo que te permite escribir código hermoso como este:
Puede ser difícil confiar en el formateo automático si usa esta técnica con frecuencia, y algunos IDEs pueden gritarle un poco sobre lo que erróneamente supone que está mal formado. Y como dice el dicho, todo es una compensación, pero supongo que no es un mal precio a pagar para evitar los males del código repetido.
fuente
Dado que hizo una consulta por curiosidad, y su pregunta no está etiquetada con un idioma específico (aunque está claro que tenía en mente los idiomas imperativos), puede valer la pena agregar que los idiomas que admiten la evaluación diferida permiten un enfoque completamente diferente. En esos idiomas, las expresiones solo se evalúan cuando es necesario, por lo que puede definir "variables" y usarlas solo cuando tenga sentido hacerlo. Por ejemplo, en un lenguaje de ficción con perezosos
let
/in
estructuras que se olvide de control de flujo y de escritura:fuente