performance - para - ¿CUANDO ES VERDAD... DESCANSO... FINALIZAR MIENTRAS que un buen diseño?
pasos del diseño (17)
¿Has programado todo usando AppleEvents ? Casi todas las rutinas individuales devuelven un OSErr cuando algo (rara vez) sale mal, pero al ser un ingeniero diligente, usted verifica todos los valores devueltos. Tu código comienza a oler mal como resultado. Se siente mejor oliendo a hacer algo como esto (esto fue hace 12 años, perdona mi memoria):
#define BEGIN_AE_HANDLER while (true) {
#define END_AE_HANDLER break; }
#AE_CHECK_ERROR(exp) if ((exp) != noErr) { break; }
/* code snippet */
OSErr err = noErr;
BEGIN_AE_HANDLER
AE_CHECK_ERROR(err = AECreateAppleEvent( /* ... */ ));
AE_CHECK_ERROR(err = AEPutAttributePtr( /* ... */ ));
/* dozens of other lines, my AppleEvents are wordy */
END_AE_HANDLER
if (err != noErr) { /* tada */ }
Y este código se lee mejor que la pila de devoluciones y refactorización de lo contrario. Déjame decirte, fue una verdadera bendición cuando el código AE estaba envuelto en clases de C ++ que funcionaban con excepciones en su lugar.
Estuve mirando un código recientemente donde el autor usó algo similar a este (pseudo-código):
WHILE TRUE
OpenAFile
IF Failed
BREAK
ENDIF
DoSomething
IF NOT OK
BREAK
ENDIF
...etc...
BREAK
END WHILE
Los argumentos para usar este diseño fueron la velocidad (debido a las rutinas de ''caídas'') y la legibilidad (sin indentaciones desagradables de múltiples FI).
Los argumentos en contra han sido que no es más que un desagradable GOTO.
Mi pregunta: ¿Es este un buen patrón de diseño?
¡EDITAR NOTA! El ejemplo anterior no pretende "hacer" nada, solo demostrar el estilo del código.
Creo que también debería agregar que probablemente un buen uso de GOTO (dentro de una función) es el manejo centralizado de errores (de nuevo, esto no sucede todo el tiempo .....). Pero en este caso solo usaría GOTO en lugar de un ciclo while. Si eso es lo que se intenta, simplemente use GOTO. También debo agregar que muchas veces tiene sentido manejar diferentes errores en diferentes lugares. Pero hay momentos en que manejar todos los errores en un lugar central es lo correcto.
(nota, no salte a otras funciones / archivos / cualquier otra cosa ... esa es la razón por la cual GOTO tiene tan mal rap)
do something if error goto errorhandler do something else if error goto errorhandler if error goto errorhandler .... errorhandler: handle errors
Algunos idiomas tienen una estructura que es mejor usar
try { do something do something else do yet something else } catch { error handling code }
Pero muchos lenguajes no tienen esta estructura ... E incluso con esta estructura, muchos lenguajes no tienen una forma de garantizar que una pieza de código se ejecute sin importar qué, si hay un error o no.
La legibilidad de este código se puede mejorar moviendo a GOTO. Eso es un signo.
OpenAFile
IF Failed GOTO TheEnd
DoSomething
IF NOT OK GOTO TheEnd
...etc...
TheEnd:
Más o menos lo que TheXenocide dijo:
WHILE TRUE
OpenAFile
IF Failed
BREAK
ENDIF
DoSomething
IF NOT OK
BREAK
ENDIF
...etc...
BREAK
END WHILE
podría ser:
try {
if (!OpenAFile()) throw new Exception(); //throws an exception? otherwise the next line would be needed
if (!DoSomething()) throw new Exception();
etc
} catch (Exception soex) {
//do nothing, but it can only come from 2 places
}
Por supuesto, si no tienes excepciones, entonces estás un poco recargado ... pero prefiero el IF.
Se supone que los bucles deben usarse en circunstancias en las que el mismo código debería poder ejecutarse más de una vez seguidas. Hay una serie de métodos para lograr esto, ninguno de los cuales es más correcto, pero para darle algunas ideas puede usar la lógica de excepción, que es útil en códigos compartimentados reutilizables porque diferentes porciones de código pueden desear tratar las fallas de manera diferente. Suponiendo que ninguno de los códigos que está llamando arroja excepciones, los lanzaría usted mismo:
OpenAFile
If Failed Then Throw New IOException("Unable to open file.")
DoSomething
If Not OK Then Throw New ApplicationException("Couldn''t DoSomething.")
alternativamente, puede permitir que la excepción natural salga del código de llamada o atrape excepciones en el método para fallar de manera segura según el contexto en el que lo use:
Public Function TryToDoStuff( ... ) As Boolean
Try
OpenAFile
DoSomething
Catch ioex As IOException
Return False
End Try
Return True
End Function
Para referencia futura, puede ser más específico con su pregunta, ya que ayudará a las personas a comprender su verdadero propósito, pero en última instancia todo el mundo tiene sus propias reglas y directrices mientras codifican y el único mandato real es que hace el trabajo. Sin embargo, los métodos anteriores son enfoques estándar porque mejoran la legibilidad, la fiabilidad y la facilidad de mantenimiento (colocar accidentalmente un descanso en el lugar equivocado es más fácil que olvidar un End Try, ya que el compilador lo notará).
Nota: Hay una circunstancia en la que se usa un bucle de esta naturaleza para realizar una sola acción: la lógica de reintento progresivo puede continuar intentando hacer algo (suponiendo que haya problemas como esperar disponibilidad o esperar la posibilidad de un error conocido), pero no parece que este sea el caso aquí y, en general, la lógica de reintento y de error también puede implementarse de maneras más sostenibles.
Editar:
El ejemplo puede estar causando cierta confusión; Es difícil discernir si está procesando más de un elemento en el ciclo o si lo está utilizando solo como un paradigma de control de flujo. Si solo es para el control de flujo, muchas de las opciones anteriores son más fáciles de mantener, aunque entiendo el deseo de evitar las declaraciones if anidadas, aún puede usar algo que no insinúe el procesamiento de elementos múltiples; lo más probable es que si su código se divide en tantas operaciones y condiciones de control de flujo, probablemente pueda mejorar su abstracción para hacer que su código sea más fácil de mantener, al mismo tiempo que se logran resultados similares al evitar enunciados if anidados.
Si esto se enfoca hacia la velocidad, está logrando una ganancia menor, si la hay, sobre las alternativas y, si es realmente tan importante para ti, entonces probablemente deberías usar un Do While (y realmente no deberíamos usar VB si la optimización es importante para ti) ya que actualmente estás evaluando innecesariamente una condición siempre verdadera que es un paso de ejecución desaprovechado (probablemente anulando el beneficio de la transición); aún así, usted está usando tantas evaluaciones condicionales como su código y, en última instancia, su pila de llamadas sigue (probablemente) creciendo y disminuyendo durante la ejecución, por lo que es más factible compartimentar las funciones y usar retornos.
En definitiva, el estilo es una elección personal, y no hay nada de malo en una declaración de interrupción, del mismo modo que no hay nada de malo con un lanzamiento o devolución; en última instancia, cada rama en el código es un goto, solo utilizamos un vocabulario diferente para describir las circunstancias en torno al salto. Si el código hace su trabajo y es fácil de mantener, no tiene nada de malo. Si está utilizando bucles en lugar de otras alternativas de control de flujo para optimizarlo, quizás deba comenzar a escribir ensamblados, entonces no hay absolutamente ningún código innecesario (si es bueno en eso; p).
Creo que todos podemos apreciar este comic , pero en última instancia, todavía se verá utilizado, incluso en comunidades de código bien pensadas. Muchos controladores de Linux los contienen y, si la gente realmente quisiera ser nazi acerca de todo esto, este sería un patrón excelente que podría cambiar la forma en que los usa, pero en última instancia, todas las alternativas son las mismas para la máquina.
"FIN WHILE" es solo una desagradable goto. "SI" es solo un desagradable goto. Puedes decir lo mismo sobre cualquier construcción de control de flujo. Eso significa que solo necesitamos tener una mejor definición de lo que es "desagradable". La regla general es que las construcciones de bucle y las salidas de subrutina pueden ir a un punto anterior en la ejecución, todas las demás deben ir a un punto posterior en la ejecución. Como las interrupciones van a un punto posterior en la ejecución, estamos bien allí.
La regla real es algo más parecido a lo legible que es. ¿Qué tan fácil es para el técnico de mantenimiento seguir el flujo de ejecución? Si es fácil, problema resuelto. Si no, entonces tenemos una mala construcción.
Esto es bastante subjetivo, pero basaría mi decisión en:
- ¿Cuánto dura el ciclo?
- ¿Qué tan cerca está la ruptura al principio o al final del ciclo?
- ¿Qué tan fácil es reordenar las cosas para mover la ruptura (o eliminarla por completo) sin repetir el código? Mover el código a otra subrutina puede ser una opción, pero puede causar estragos en el alcance, o incluso tener cosas muy juntas que, conceptualmente, están muy juntas.
Dicho eso, lo que parece que estás haciendo es implementar excepciones. Si tiene un lenguaje que permite excepciones, puede proporcionar un método más limpio. Si no tiene un lenguaje que permita excepciones, entonces lo que está haciendo puede ser lo más limpio posible.
A veces es un muy buen diseño. Ver Programación estructurada con declaraciones Goto por Donald Knuth para algunos ejemplos. Utilizo esta idea básica a menudo para bucles que se ejecutan "n y medio veces", especialmente bucles de lectura / proceso. Sin embargo, generalmente intento tener solo una declaración de interrupción. Esto hace que sea más fácil razonar sobre el estado del programa después de que termine el ciclo.
Creo que el patrón de diseño es excelente, pero tu ejemplo es pobre.
¿Cuántas veces ves bucles como
Read a line from a file while (not end of file) do stuff read a line from a file end while
En este caso, está leyendo desde el archivo en dos lugares diferentes (por lo tanto, se duplicará). ¿Qué sucede si determina que necesita cambiar la variable en la que está leyendo, necesita cambiar la variable que apunta al archivo, o alguna otra cosa como el número de bytes para leer, el carácter de división del registro, etc. hazlo en dos lugares
aún más dramático es un ejemplo en SQL
fetch from cursor into cola, colb, colc while not end of cursor do stuff fetch from cursor into cola, colb, colc end while
En SQL, ¿qué sucede si necesita una columna adicional? Tienes que cambiar el código en dos lugares ...
De todos modos, estos son solo algunos ejemplos. Hay muchos tipos de bucles como este. Los veo a menudo por cosas como romper una cuerda, etc.
apply regular expression pattern to string while match do something apply regular expression pattern to string end while
Si el patrón cambia, debe recordar cambiar el patrón en ambos lugares ...
¿No es más fácil simplemente ir?
while true (or 1=1 or whatever) prepare results for statement once if (do check once) break end if do statement once end while
Yo diría que usar un goto en este patrón es un uso válido de un GOTO. No son malvados si se usan correctamente. El problema es que la gente los usa para saltar sobre el código (como en el medio de una función diferente). Después de todo, muchos lenguajes ensamblados solo tienen GOTO (a través de instrucciones de salto y bifurcación).
En cuanto a tu ejemplo, es malo porque hay un montón de salidas. Probablemente debería estar en una función
function codeab (filename) { open file if failed return error do something if failed return error return data structure with result } while (whatever the loop condition actually is) { result = codeab(filename) if error additional error handling do something with result }
Está más claro. Pero no sé mucho sobre el código que publicaste, así que no puedo decirlo con certeza. Pero el patrón de salir del medio del ciclo es muy útil y es un "buen" uso de GOTO.
De forma rutinaria uso break y continue en bucles porque usarlos significa que no tengo que usar algo peor. La cantidad de código que tiene que escribir en algunas circunstancias para implementar la reacción instintiva "buen diseño" puede ser ridículo y contraproducente. El código (con break y continue) es legible y fácil de entender. Realmente no entiendo por qué es considerado por cualquiera como un mal diseño.
Las interrupciones y las continuas no son equivalentes a los gotos y no merecen la mala cobertura. Son simplemente azúcar sintáctica para reemplazar un if / else mucho más complejo en el resto del bloque.
Múltiples puntos de salida generalmente deben ser evitados. Sin embargo, si lo mantienes corto y lo suficientemente simple, esto puede usarse sin ser demasiado peligroso. Usa lo más legible A veces hay que violar una buena regla de programación para evitar violar otra regla más importante.
Me gusta este diseño La única forma en que el código fluye es de arriba hacia abajo (o en el bucle). Es por eso que no es tan malo como un GOTO.
Además, en lo que respecta a la legibilidad, es mejor que la alternativa de if anidados.
No, solo está abusando de un bucle (a menos que realmente esté bucleando algo, pero no se ve así). Esto debería ser una subrutina, y cada BREAK
debería ser un RETURN
. Que ambos evitan envolver todo el cuerpo en un IF y hace que la intención del cheque sea obvia.
Sí, especialmente para emular bucles do-while-do (construcciones Do-Loop en VB) en idiomas que no tienen el constructo, como java. Estos bucles son algo entre bucles do-while y while-do. Los casos donde esta es una buena idea a menudo son fáciles de detectar, porque tiene alguna duplicación de código obvia antes y en el ciclo (para while-do) y en el ciclo y después (para do-while). Al mover la guarda en el cuerpo del bucle, puede reducir la duplicación.
Del mismo modo, es mejor regresar de una función cuando aparece una condición de error, en lugar de agregar otra capa:
int f(int x, int y) {
if (y < 0) { // or some other error condition
return 0; // or throw exception
}
... // perform operations
}
v.
int f(int x, int y) {
if (0 <= y) { // or some other error condition
... // perform operations
} else {
return 0;
}
}
Ver también: Code 16 Completa el Capítulo 16 sobre la construcción de bucles.
a menos que haya un descanso al final, ¡este es un ciclo infinito!
do { ... } while(false); // or whatever it''s written in VB
es una mejor alternativa
Un artículo fascinante es Knuth''s "Programación estructurada con ir a declaraciones" de 1974 (disponible en su libro "Programación alfabetizada", y probablemente también en otros lugares). Discute, entre otras cosas, las formas controladas de salir de los bucles, y (sin usar el término) la instrucción de bucle y medio.
Ada también proporciona construcciones de bucle, que incluyen
loopname:
loop
...
exit loopname when ...condition...;
...
end loop loopname;
El código de la pregunta original es similar a este en la intención.
mi maestra me dijo que es un mal estilo, pero me parece muy legible. Por ejemplo, cuando lee archivos de texto y trata de atrapar caracteres.
var c: char; arco: texto;
begin assign (arch, ''text.txt''); restablecer (arco); mientras que verdadero comienza leer (arco, c); si no (c en [# 10, # 12, # 13, # 32]) luego pDoSomething (c); si c = # 26 {o eof (arch)} entonces rompa; fin; fin.