A menudo me encuentro con métodos / funciones que tienen un parámetro booleano adicional que controla si se produce una excepción en caso de error o se devuelve un valor nulo.
Ya hay discusiones sobre cuál es la mejor opción en cada caso, así que no nos centremos en esto aquí. Ver, por ejemplo, ¿ Devolver valor mágico, lanzar excepción o devolver falso en caso de falla?
En su lugar, supongamos que hay una buena razón por la que queremos apoyar en ambos sentidos.
Personalmente, creo que dicho método debería dividirse en dos: uno que arroja una excepción en caso de falla, el otro que devuelve nulo en caso de falla.
Entonces, ¿cuál es mejor?
A: Un método con $exception_on_failure
parámetro.
/**
* @param int $id
* @param bool $exception_on_failure
*
* @return Item|null
* The item, or null if not found and $exception_on_failure is false.
* @throws NoSuchItemException
* Thrown if item not found, and $exception_on_failure is true.
*/
function loadItem(int $id, bool $exception_on_failure): ?Item;
B: dos métodos distintos.
/**
* @param int $id
*
* @return Item|null
* The item, or null if not found.
*/
function loadItemOrNull(int $id): ?Item;
/**
* @param int $id
*
* @return Item
* The item, if found (exception otherwise).
*
* @throws NoSuchItemException
* Thrown if item not found.
*/
function loadItem(int $id): Item;
EDITAR: C: ¿Algo más?
Muchas personas han sugerido otras opciones o afirman que tanto A como B tienen fallas. Tales sugerencias u opiniones son bienvenidas, relevantes y útiles. Una respuesta completa puede contener dicha información adicional, pero también abordará la pregunta principal de si un parámetro para cambiar la firma / comportamiento es una buena idea.
Notas
En caso de que alguien se pregunte: los ejemplos están en PHP. Pero creo que la pregunta se aplica a todos los lenguajes siempre que sean algo similares a PHP o Java.
fuente
loadItemOrNull(id)
considerar siloadItemOr(id, defaultItem)
tiene sentido para usted. Si el artículo es una Cadena o número, a menudo lo hace.Respuestas:
Tienes razón: dos métodos son mucho mejores para eso, por varias razones:
En Java, la firma del método que potencialmente arroja una excepción incluirá esta excepción; el otro método no lo hará. Esto deja particularmente claro qué esperar de uno y otro.
En lenguajes como C # donde la firma del método no dice nada acerca de las excepciones, los métodos públicos aún deben documentarse, y dicha documentación incluye las excepciones. Documentar un solo método no sería fácil.
Su ejemplo es perfecto: los comentarios en la segunda parte del código se ven mucho más claros, e incluso resumiría "El artículo, si se encuentra (excepción de lo contrario)" hasta "El artículo": la presencia de una posible excepción y el La descripción que le diste se explica por sí misma.
En el caso de un único método, hay pocos casos en los que le gustaría alternar el valor del parámetro booleano en tiempo de ejecución , y si lo hace, significaría que la persona que llama tendrá que manejar ambos casos (una
null
respuesta y la excepción ), lo que hace que el código sea mucho más difícil de lo necesario. Dado que la elección se realiza no en tiempo de ejecución, sino al escribir el código, dos métodos tienen mucho sentido.Algunos marcos, como .NET Framework, han establecido convenciones para esta situación y la resuelven con dos métodos, tal como usted sugirió. La única diferencia es que usan un patrón explicado por Ewan en su respuesta , por lo que
int.Parse(string): int
arroja una excepción, mientrasint.TryParse(string, out int): bool
que no lo hace. Esta convención de nomenclatura es muy sólida en la comunidad de .NET y debe seguirse siempre que el código coincida con la situación que usted describe.fuente
int.Parse()
se considera una decisión de diseño realmente mala, razón por la cual el equipo de .NET siguió adelante e implementóTryParse
.->itemExists($id)
antes de llamar->loadItem($id)
. O tal vez es solo una elección hecha por otras personas en el pasado, y tenemos que darla por sentado.data Maybe a = Just a | Nothing
).Una variación interesante es el lenguaje Swift. En Swift, declara una función como "lanzamiento", es decir, está permitido lanzar errores (similar pero no exactamente igual a las excepciones de Java). La persona que llama puede llamar a esta función de cuatro maneras:
a. En una declaración try / catch, captura y manejo de la excepción.
si. Si la persona que llama se declara como lanzando, simplemente llámelo, y cualquier error arrojado se convierte en un error lanzado por la persona que llama.
do. Marcar la llamada de función con lo
try!
que significa "Estoy seguro de que esta llamada no se lanzará". Si la función llamada hace un tiro, la aplicación está garantizada a accidente.re. Marcar la llamada a la función con lo
try?
que significa "Sé que la función puede lanzar pero no me importa qué error se lanza exactamente". Esto cambia el valor de retorno de la función de tipo "T
" a tipo "optional<T>
", y una excepción lanzada se convierte en retorno nulo.(a) y (d) son los casos sobre los que está preguntando. ¿Qué pasa si la función puede devolver nulo y puede lanzar excepciones? En ese caso, el tipo del valor de retorno sería "
optional<R>
" para algún tipo R, y (d) cambiaría esto a "optional<optional<R>>
", que es un poco críptico y difícil de entender pero completamente correcto. Y una función Swift puede regresaroptional<Void>
, por lo que (d) puede usarse para lanzar funciones que devuelven Void.fuente
Me gusta el
bool TryMethodName(out returnValue)
acercamiento.Le da una indicación concluyente de si el método tuvo éxito o no y los nombres coinciden envolviendo el método de lanzamiento de excepción en un bloque try catch.
Si solo devuelve nulo, no sabe si falló o si el valor de retorno fue legítimamente nulo.
p.ej:
ejemplo de uso (out significa pasar por referencia sin inicializar)
fuente
bool TryMethodName(out returnValue)
enfoque" en el ejemplo original?Por lo general, un parámetro booleano indica que una función se puede dividir en dos y, por regla general, siempre debe dividirse en lugar de pasar un booleano.
fuente
b
: en lugar de llamarfoo(…, b);
, tiene que escribirif (b) then foo_x(…) else foo_y(…);
y repetir los otros argumentos, o algo así como((b) ? foo_x : foo_y)(…)
si el lenguaje tiene un operador ternario y funciones / métodos de primera clase. Y esto puede incluso repetirse desde varios lugares diferentes en el programa.if (myGrandmaMadeCookies) eatThem() else makeFood()
que sí podría envolver en otra función como estacheckIfShouldCook(myGrandmaMadeCookies)
. Me pregunto si el matiz que mencionó tiene que ver con si estamos pasando un booleano sobre cómo queremos que se comporte la función, como en la pregunta original, frente a si estamos pasando alguna información sobre nuestro modelo, como en el abuela ejemplo que acabo de dar.Clean Code recomienda evitar argumentos de bandera booleana, porque su significado es opaco en el sitio de la llamada:
mientras que métodos separados pueden usar nombres expresivos:
fuente
Si tuviera que usar A o B, usaría B, dos métodos separados, por las razones indicadas en la respuesta de Arseni Mourzenkos .
Pero hay otro método 1 : en Java se llama
Optional
, pero puede aplicar el mismo concepto a otros lenguajes donde puede definir sus propios tipos, si el lenguaje aún no proporciona una clase similar.Usted define su método de esta manera:
En su método, usted regresa
Optional.of(item)
si encontró el artículo o regresaOptional.empty()
en caso de que no lo haga. Nunca lanzas 2 y nunca regresasnull
.Para el cliente de su método es algo así
null
, pero con la gran diferencia que obliga a pensar en el caso de elementos faltantes. El usuario no puede simplemente ignorar el hecho de que puede no haber resultado. Para sacar el elemento delOptional
se requiere una acción explícita:loadItem(1).get()
arrojaráNoSuchElementException
o devolverá el artículo encontrado.loadItem(1).orElseThrow(() -> new MyException("No item 1"))
que usar una excepción personalizada si la devoluciónOptional
está vacía.loadItem(1).orElse(defaultItem)
devuelve el elemento encontrado o el pasadodefaultItem
(que también puede sernull
) sin lanzar una excepción.loadItem(1).map(Item::getName)
volvería a devolver unOptional
con el nombre de los elementos, si está presente.Optional
.La ventaja es que ahora depende del código del cliente lo que sucederá si no hay un artículo y aún así solo necesita proporcionar un método único .
1 Supongo que es algo así como la respuesta
try?
en gnasher729s, pero no está del todo claro para mí, ya que no conozco Swift y parece ser una característica del lenguaje, por lo que es específico de Swift.2 Puede lanzar excepciones por otros motivos, por ejemplo, si no sabe si hay un elemento o no porque tuvo problemas para comunicarse con la base de datos.
fuente
Como otro punto de acuerdo con el uso de dos métodos, esto puede ser muy fácil de implementar. Escribiré mi ejemplo en C #, ya que no conozco la sintaxis de PHP.
fuente
Prefiero dos métodos, de esta manera, no solo me dirá que está devolviendo un valor, sino qué valor exactamente.
TryDoSomething () tampoco es un mal patrón.
Estoy más acostumbrado a ver el primero en las interacciones de la base de datos, mientras que el segundo se usa más en el análisis.
Como otros ya te han dicho, los parámetros de la bandera son generalmente un olor a código (los olores a código no significan que estás haciendo algo mal, solo que hay una probabilidad de que lo estés haciendo mal). Aunque lo nombras con precisión, a veces hay matices que dificultan saber cómo usar esa bandera y terminas mirando dentro del cuerpo del método.
fuente
Mientras que otras personas sugieren lo contrario, sugeriría que tener un método con un parámetro que indique cómo deben manejarse los errores es mejor que requerir el uso de métodos separados para los dos escenarios de uso. Si bien puede ser deseable tener también puntos de entrada distintos para los usos de "error arroja" versus "error devuelve indicación de error", tener un punto de entrada que pueda servir a ambos patrones de uso evitará la duplicación de código en los métodos de envoltura. Como un simple ejemplo, si uno tiene funciones:
y uno quiere funciones que se comporten de la misma manera pero con x entre paréntesis, sería necesario escribir dos conjuntos de funciones de contenedor:
Si hay un argumento sobre cómo manejar los errores, solo se necesitaría un contenedor:
Si el contenedor es lo suficientemente simple, la duplicación del código podría no ser tan mala, pero si alguna vez necesita cambiar, duplicar el código a su vez requerirá duplicar cualquier cambio. Incluso si uno opta por agregar puntos de entrada que no usan el argumento adicional:
se puede mantener toda la "lógica de negocios" en un método: el que acepta un argumento que indica qué hacer en caso de falla.
fuente
Creo que todas las respuestas que explican que no debe tener una bandera que controle si se produce una excepción o no son buenas. Su consejo sería mi consejo para cualquiera que sienta la necesidad de hacer esa pregunta.
Sin embargo, quería señalar que hay una excepción significativa a esta regla. Una vez que esté lo suficientemente avanzado como para comenzar a desarrollar API que utilizarán cientos de otras personas, hay casos en los que es posible que desee proporcionar dicha marca. Cuando estás escribiendo una API, no solo estás escribiendo a los puristas. Estás escribiendo a clientes reales que tienen deseos reales. Parte de su trabajo es hacerlos felices con su API. Eso se convierte en un problema social, en lugar de un problema de programación, y a veces los problemas sociales dictan soluciones que serían menos que ideales como soluciones de programación por sí mismas.
Es posible que tenga dos usuarios distintos de su API, uno de los cuales quiere excepciones y otro que no. Un ejemplo de la vida real donde esto podría ocurrir es con una biblioteca que se usa tanto en desarrollo como en un sistema integrado. En entornos de desarrollo, los usuarios probablemente deseen tener excepciones en todas partes. Las excepciones son muy populares para manejar situaciones inesperadas. Sin embargo, están prohibidos en muchas situaciones incrustadas porque son demasiado difíciles de analizar en tiempo real. Cuando realmente le importa no solo el tiempo promedio que lleva ejecutar su función, sino también el tiempo que le lleva a cualquier diversión individual, la idea de desenrollar la pila en cualquier lugar arbitrario es muy indeseable.
Un ejemplo de la vida real para mí: una biblioteca de matemáticas. Si su biblioteca de matemáticas tiene una
Vector
clase que admite obtener un vector unitario en la misma dirección, debe poder dividir por la magnitud. Si la magnitud es 0, tiene una situación de división por cero. En desarrollo quieres atraparlos . Realmente no quieres sorprender dividir entre 0 dando vueltas. Lanzar una excepción es una solución muy popular para esto. Casi nunca sucede (es un comportamiento verdaderamente excepcional), pero cuando sucede, quieres saberlo.En la plataforma integrada, no desea esas excepciones. Tendría más sentido hacer un chequeo
if (this->mag() == 0) return Vector(0, 0, 0);
. De hecho, veo esto en código real.Ahora piense desde la perspectiva de un negocio. Puede intentar enseñar dos formas diferentes de usar una API:
Esto satisface las opiniones de la mayoría de las respuestas aquí, pero desde la perspectiva de la compañía esto no es deseable. Los desarrolladores integrados tendrán que aprender un estilo de codificación, y los desarrolladores de desarrollo tendrán que aprender otro. Esto puede ser bastante molesto y obliga a las personas a una sola forma de pensar. Potencialmente peor: ¿cómo se demuestra que la versión incrustada no incluye ningún código de excepción? La versión de lanzamiento se puede mezclar fácilmente, escondiéndose en algún lugar de su código hasta que una costosa Junta de revisión de fallas lo encuentre.
Si, por otro lado, tiene una bandera que activa o desactiva el manejo de excepciones, ambos grupos pueden aprender exactamente el mismo estilo de codificación. De hecho, en muchos casos, incluso puede usar el código de un grupo en los proyectos del otro grupo. En el caso de este código
unit()
, si realmente le importa lo que sucede en un caso dividido por 0, debería haber escrito la prueba usted mismo. Por lo tanto, si lo mueve a un sistema integrado, donde solo obtiene malos resultados, ese comportamiento es "sensato". Ahora, desde una perspectiva comercial, entreno a mis codificadores una vez, y usan las mismas API y los mismos estilos en todas las partes de mi negocio.En la gran mayoría de los casos, desea seguir los consejos de otros: use modismos similares
tryGetUnit()
o similares para funciones que no arrojen excepciones y, en su lugar, devuelva un valor centinela como nulo. Si cree que los usuarios pueden querer usar tanto el código de manejo de excepciones como el de manejo de excepciones uno al lado del otro dentro de un solo programa, quédese con latryGetUnit()
notación. Sin embargo, hay un caso de esquina donde la realidad de los negocios puede mejorar el uso de una bandera. Si te encuentras en ese caso de esquina, no tengas miedo de tirar las "reglas" por el camino. ¡Para eso están las reglas!fuente