He visto una práctica de vez en cuando que "se siente" mal, pero no puedo expresar lo que está mal al respecto. O tal vez es solo mi prejuicio. Aquí va:
Un desarrollador define un método con un valor booleano como uno de sus parámetros, y ese método llama a otro, y así sucesivamente, y eventualmente ese valor booleano se usa únicamente para determinar si se debe tomar una determinada acción. Esto podría usarse, por ejemplo, para permitir la acción solo si el usuario tiene ciertos derechos, o tal vez si estamos (o no) en modo de prueba o modo por lotes o modo en vivo, o tal vez solo cuando el sistema está en un cierto estado
Bueno, siempre hay otra forma de hacerlo, ya sea preguntando cuándo es el momento de tomar la acción (en lugar de pasar el parámetro), o teniendo múltiples versiones del método, o múltiples implementaciones de la clase, etc. Mi pregunta no es No es tanto cómo mejorar esto, sino más bien si realmente está mal o no (como sospecho), y si es así, qué tiene de malo.
Respuestas:
Sí, es probable que sea un olor a código, lo que llevaría a un código que no se puede mantener y que es difícil de entender y que tiene una menor probabilidad de ser reutilizado fácilmente.
Como otros carteles han señalado, el contexto lo es todo (no entre con mano dura si es único o si la práctica ha sido reconocida como una deuda técnica incurrida deliberadamente para ser re-factorizada más adelante), pero hablando en términos generales si se pasa un parámetro en una función que selecciona un comportamiento específico para ser ejecutado, luego se requiere un mayor refinamiento gradual; Romper esta función en funciones más pequeñas producirá funciones más altamente cohesivas.
Entonces, ¿qué es una función altamente cohesiva?
Es una función que hace una cosa y solo una cosa.
El problema con un parámetro pasado como lo describe, es que la función está haciendo más de dos cosas; puede o no verificar los derechos de acceso de los usuarios dependiendo del estado del parámetro booleano, luego, dependiendo de ese árbol de decisión, llevará a cabo una funcionalidad.
Sería mejor separar las preocupaciones de Control de acceso de las preocupaciones de Tarea, Acción o Comando.
Como ya ha notado, el entrelazamiento de estas preocupaciones parece estar apagado.
Entonces, la noción de Cohesividad nos ayuda a identificar que la función en cuestión no es altamente cohesiva y que podríamos refactorizar el código para producir un conjunto de funciones más cohesivas.
Entonces la pregunta podría ser reformulada; Dado que todos estamos de acuerdo en que es mejor evitar pasar parámetros de selección de comportamiento, ¿cómo podemos mejorar las cosas?
Me desharía del parámetro por completo. Tener la capacidad de desactivar el control de acceso incluso para las pruebas es un riesgo de seguridad potencial. Para fines de prueba, puede bloquear o simular la verificación de acceso para probar los escenarios de acceso permitido y acceso denegado.
Ref: Cohesión (informática)
fuente
Dejé de usar este patrón hace mucho tiempo, por una razón muy simple; costo de mantenimiento. Varias veces descubrí que tenía alguna función
frobnicate(something, forwards_flag)
que se llamaba muchas veces en mi código, y que necesitaba ubicar todos los lugares en el código dondefalse
se pasaba el valor como valor deforwards_flag
. No puede buscarlos fácilmente, por lo que esto se convierte en un dolor de cabeza de mantenimiento. Y si necesita hacer una corrección de errores en cada uno de esos sitios, puede tener un problema desafortunado si se pierde uno.Pero este problema específico se soluciona fácilmente sin cambiar fundamentalmente el enfoque:
Con este código, uno solo necesitaría buscar instancias de
FrobnicateBackwards
. Si bien es posible que haya algún código que asigne esto a una variable, por lo que debe seguir algunos hilos de control, creo que en la práctica esto es lo suficientemente raro como para que esta alternativa funcione bien.Sin embargo, hay otro problema al pasar la bandera de esta manera, al menos en principio. Esto es que algunos (solo algunos) sistemas que tienen este diseño pueden estar exponiendo demasiado conocimiento sobre los detalles de implementación de las partes profundamente anidadas del código (que usa la bandera) a las capas externas (que necesitan saber qué valor pasar en esta bandera) Para utilizar la terminología de Larry Constantine, este diseño puede tener un acoplamiento demasiado fuerte entre el colocador y el usuario de la bandera booleana. Franky, aunque es difícil de decir con cierto grado de certeza sobre esta pregunta sin saber más sobre el código base.
Para abordar los ejemplos específicos que da, me preocuparía un poco en cada uno, pero principalmente por razones de riesgo / corrección. Es decir, si su sistema necesita pasar banderas que indican en qué estado se encuentra el sistema, puede encontrar que tiene un código que debería haber tenido en cuenta esto pero no verifica el parámetro (porque no se pasó a esta función). Entonces tiene un error porque alguien omitió pasar el parámetro.
También vale la pena admitir que un indicador de estado del sistema que debe pasarse a casi todas las funciones es, de hecho, esencialmente una variable global. Se aplicarán muchas de las desventajas de una variable global. Creo que en muchos casos es una mejor práctica encapsular el conocimiento del estado del sistema (o las credenciales del usuario, o la identidad del sistema) dentro de un objeto que es responsable de actuar correctamente sobre la base de esos datos. Luego, pasa una referencia a ese objeto en lugar de los datos sin procesar. El concepto clave aquí es la encapsulación .
fuente
bool
que se agregaron parámetros adicionales más tarde y las llamadas comienzan a verseDoSomething( myObject, false, false, true, false )
. Es imposible descubrir qué significan los argumentos booleanos adicionales, mientras que con valores de enumeración con nombres significativos, es fácil.Esto no es necesariamente incorrecto, pero puede representar un olor a código .
El escenario básico que debe evitarse con respecto a los parámetros booleanos es:
Luego, cuando llama, normalmente llama
foo(false)
yfoo(true)
depende del comportamiento exacto que desee.Esto es realmente un problema porque es un caso de mala cohesión. Estás creando una dependencia entre métodos que no es realmente necesaria.
Lo que debe hacer en este caso es irse
doThis
y,doThat
como métodos separados y públicos, hacer:o
De esa manera, deja la decisión correcta a la persona que llama (exactamente como si estuviera pasando un parámetro booleano) sin crear un acoplamiento.
Por supuesto, no todos los parámetros booleanos se usan de manera tan mala, pero definitivamente es un olor a código y tienes razón para sospechar si ves mucho en el código fuente.
Este es solo un ejemplo de cómo resolver este problema basado en los ejemplos que escribí. Hay otros casos en los que será necesario un enfoque diferente.
Hay un buen artículo de Martin Fowler que explica con más detalles la misma idea.
PD: si el método en
foo
lugar de llamar a dos métodos simples tuvo una implementación más compleja, entonces todo lo que tiene que hacer es aplicar una pequeña refactorización de métodos de extracción para que el código resultante sea similar a la implementación de lofoo
que escribí.fuente
if (flag) doThat()
dentrofoo()
es legítimo. Impulsar la decisión de invocardoThat()
a cada persona que llama obliga a la repetición que tendrá que ser eliminada si luego descubre algunos métodos, elflag
comportamiento también necesita llamardoTheOther()
. Prefiero poner dependencias entre métodos en la misma clase que tener que buscar a todos los que llaman más tarde.doOne
ydoBoth
(para el caso falso y verdadero, respectivamente) o el uso de un tipo de enumeración por separado, como lo sugiere James YoungmandoOne()
odoBoth()
decisión. Las subrutinas / funciones / métodos tienen argumentos para que su comportamiento pueda variar. Usar una enumeración para una condición verdaderamente booleana se parece mucho a repetirse si el nombre del argumento ya explica lo que hace.En primer lugar: la programación no es tanto una ciencia como un arte. Por lo tanto, rara vez hay una forma "incorrecta" y una "correcta" de programar. La mayoría de los estándares de codificación son meramente "preferencias" que algunos programadores consideran útiles; pero finalmente son bastante arbitrarios. Por lo tanto, nunca etiquetaría una elección de parámetro como "incorrecta" en sí misma, y ciertamente no es algo tan genérico y útil como un parámetro booleano. El uso de un
boolean
(o unint
, para el caso) para encapsular el estado es perfectamente justificable en muchos casos.Las decisiones de codificación, en general, deben basarse principalmente en el rendimiento y la capacidad de mantenimiento. Si el rendimiento no está en juego (y no puedo imaginar cómo podría ser en sus ejemplos), entonces su siguiente consideración debería ser: ¿qué tan fácil será para mí (o un futuro redactor) mantenerlo? ¿Es intuitivo y comprensible? ¿Está aislado? Su ejemplo de las llamadas a funciones encadenadas de hecho parecen potencialmente frágil en este sentido: si decide cambiar su
bIsUp
abIsDown
, cuántos lugares distintos en el código tendrá que ser cambiado también? Además, ¿está aumentando su lista de paramater? Si su función tiene 17 parámetros, entonces la legibilidad es un problema, y debe reconsiderar si está apreciando los beneficios de la arquitectura orientada a objetos.fuente
Creo que el artículo del código Robert C Martins Clean establece que debe eliminar los argumentos booleanos siempre que sea posible, ya que muestran que un método hace más de una cosa. Un método debería hacer una cosa y solo una cosa, creo que es uno de sus lemas.
fuente
Creo que lo más importante aquí es ser práctico.
Cuando el valor booleano determina el comportamiento completo, simplemente haga un segundo método.
Cuando el booleano solo determina un poco de comportamiento en el medio, es posible que desee mantenerlo en uno para reducir la duplicación de código. Siempre que sea posible, incluso podría dividir el método en tres: dos métodos de llamada para cualquier opción booleana y uno que haga la mayor parte del trabajo.
Por ejemplo:
Por supuesto, en la práctica siempre tendrás un punto entre estos extremos. Por lo general, sigo con lo que parece correcto, pero prefiero equivocarme con menos duplicación de código.
fuente
FooInternal
en el futuro, ¿entonces qué?Foo1
se convierte en:{ doWork(); HandleTrueCase(); doMoreWork() }
. Idealmente, las funcionesdoWork
ydoMoreWork
se dividen en (uno o más) fragmentos significativos de acciones discretas (es decir, como funciones separadas), no solo dos funciones en aras de la división.Me gusta el enfoque de personalizar el comportamiento a través de métodos de creación que devuelven instancias inmutables. Así es como lo usa Guava
Splitter
:Los beneficios de esto son:
Splitter
. Nunca pondríassomeVaguelyStringRelatedOperation(List<Entity> myEntities)
en una clase llamadaSplitter
, pero pensarías en ponerlo como un método estático en unaStringUtils
clase.true
ofalse
por un método para obtener el comportamiento correcto.fuente
Definitivamente un código de olor . Si no viola el Principio de Responsabilidad Única, entonces probablemente viola Tell, Don't Ask . Considerar:
Si resulta que no viola uno de esos dos principios, aún debe usar una enumeración. Las banderas booleanas son el equivalente booleano de los números mágicos .
foo(false)
tiene tanto sentido comobar(42)
. Las enumeraciones pueden ser útiles para el Patrón de estrategia y tienen la flexibilidad de permitirle agregar otra estrategia. (Solo recuerde nombrarlos apropiadamente ).Su ejemplo particular me molesta especialmente. ¿Por qué se pasa esta bandera por tantos métodos? Esto suena como si necesita dividir su parámetro en subclases .
fuente
TL; DR: no use argumentos booleanos.
Vea a continuación por qué son malos y cómo reemplazarlos (en negrita).
Los argumentos booleanos son muy difíciles de leer y, por lo tanto, difíciles de mantener. El principal problema es que el propósito es generalmente claro cuando lee la firma del método donde se nombra el argumento. Sin embargo, nombrar un parámetro generalmente no se requiere en la mayoría de los idiomas. Por lo tanto, tendrá antipatrones como
RSACryptoServiceProvider#encrypt(Byte[], Boolean)
donde el parámetro booleano determina qué tipo de cifrado se utilizará en la función.Entonces recibirá una llamada como:
donde el lector tiene que buscar la firma del método simplemente para determinar qué demonios
true
podría significar realmente. Por supuesto, pasar un número entero es igual de malo:te diría lo mismo, o más bien: tan poco. Incluso si define constantes para el entero, los usuarios de la función pueden simplemente ignorarlas y seguir usando valores literales.
La mejor manera de resolver esto es usar una enumeración . Si tiene que pasar una enumeración
RSAPadding
con dos valores:OAEP
oPKCS1_V1_5
entonces inmediatamente podrá leer el código:Los booleanos solo pueden tener dos valores. Esto significa que si tiene una tercera opción, entonces tendría que refactorizar su firma. En general, esto no se puede realizar fácilmente si la compatibilidad con versiones anteriores es un problema, por lo que tendría que ampliar cualquier clase pública con otro método público. Esto es lo que finalmente hizo Microsoft cuando introdujeron
RSACryptoServiceProvider#encrypt(Byte[], RSAEncryptionPadding)
dónde usaron una enumeración (o al menos una clase que imitaba una enumeración) en lugar de un booleano.Incluso puede ser más fácil usar un objeto completo o una interfaz como parámetro, en caso de que el parámetro en sí mismo deba parametrizarse. En el ejemplo anterior, el relleno OAEP en sí mismo podría parametrizarse con el valor hash para usarlo internamente. Tenga en cuenta que ahora hay 6 algoritmos hash SHA-2 y 4 algoritmos hash SHA-3, por lo que la cantidad de valores de enumeración puede explotar si solo usa una enumeración única en lugar de parámetros (esto es posiblemente lo próximo que Microsoft descubrirá) )
Los parámetros booleanos también pueden indicar que el método o clase no está bien diseñado. Al igual que con el ejemplo anterior: cualquier biblioteca criptográfica que no sea .NET no utiliza ningún indicador de relleno en la firma del método.
Casi todos los gurús del software que me gustan advierten contra los argumentos booleanos. Por ejemplo, Joshua Bloch advierte contra ellos en el muy apreciado libro "Effective Java". En general, simplemente no deben usarse. Se podría argumentar que podrían usarse si el caso es que hay un parámetro que es fácil de entender. Pero incluso entonces:
Bit.set(boolean)
probablemente se implementa mejor utilizando dos métodos :Bit.set()
yBit.unset()
.Si no puede refactorizar directamente el código, puede definir constantes para al menos hacerlas más legibles:
es mucho más legible que:
incluso si prefieres tener:
en lugar.
fuente
Me sorprende que nadie haya mencionado los parámetros con nombre .
El problema que veo con las banderas booleanas es que dañan la legibilidad. Por ejemplo, qué hace
true
en¿hacer? No tengo idea. Pero que pasa:
Ahora está claro qué debe hacer el parámetro que estamos pasando: le estamos diciendo a la función que guarde sus cambios. En este caso, si la clase no es pública, creo que el parámetro booleano está bien.
Por supuesto, no puede obligar a los usuarios de su clase a usar parámetros con nombre. Por esta razón,
enum
generalmente son preferibles uno o dos métodos separados, dependiendo de qué tan común sea su caso predeterminado. Esto es exactamente lo que hace .Net:fuente
myFunction(true)
podría estar escrito, es un código- oler.SaveBig
nombre. Se puede fastidiar cualquier código, este tipo de error no es específico de los parámetros con nombre.Si parece un olor a código, se siente como un olor a código y, bueno, huele a olor a código, probablemente sea un olor a código.
Lo que quieres hacer es:
1) Evite los métodos con efectos secundarios.
2) Manejar los estados necesarios con una máquina de estado formal central (como esta ).
fuente
Estoy de acuerdo con todas las preocupaciones de usar los parámetros booleanos para no determinar el rendimiento para; mejorar, legibilidad, confiabilidad, reducir la complejidad, reducir los riesgos de una pobre encapsulación y cohesión y reducir el costo total de propiedad con capacidad de mantenimiento.
Comencé a diseñar hardware a mediados de los 70 que ahora llamamos SCADA (control de supervisión y adquisición de datos) y estos fueron hardware afinado con código de máquina en EPROM que ejecuta controles remotos macro y recopila datos de alta velocidad.
La lógica se llama máquinas Mealey & Moore , que ahora llamamos máquinas de estado finito . Esto también debe hacerse en las mismas reglas que anteriormente, a menos que sea una máquina en tiempo real con un tiempo de ejecución finito y luego se deben hacer accesos directos para cumplir el propósito.
Los datos son síncronos pero los comandos son asíncronos y la lógica de comandos sigue la lógica booleana sin memoria pero con comandos secuenciales basados en la memoria del estado anterior, presente y deseado. Para que eso funcione en el lenguaje de máquina más eficiente (solo 64kB), se tuvo mucho cuidado al definir cada proceso de una manera heurística de IBM HIPO. Eso a veces significaba pasar variables booleanas y hacer ramas indexadas.
Pero ahora con mucha memoria y facilidad de OOK, la encapsulación es un ingrediente esencial hoy en día, pero una penalización cuando el código se contaba en bytes para el código de máquina SCADA en tiempo real.
fuente
No es necesariamente incorrecto, pero en su ejemplo concreto de la acción que depende de algún atributo del "usuario", pasaría por una referencia al usuario en lugar de una bandera.
Esto aclara y ayuda de varias maneras.
Cualquiera que lea la declaración de invocación se dará cuenta de que el resultado cambiará dependiendo del usuario.
En la función que finalmente se llama, puede implementar fácilmente reglas comerciales más complejas porque puede acceder a cualquiera de los atributos de los usuarios.
Si una función / método en la "cadena" hace algo diferente dependiendo de un atributo del usuario, es muy probable que se introduzca una dependencia similar en los atributos del usuario en algunos de los otros métodos de la "cadena".
fuente
La mayoría de las veces, consideraría esta mala codificación. Sin embargo, se me ocurren dos casos en los que esta podría ser una buena práctica. Como hay muchas respuestas que ya dicen por qué es malo, ofrezco dos veces cuando podría ser bueno:
La primera es si cada llamada en la secuencia tiene sentido por derecho propio. Tendría sentido si el código de llamada se pudiera cambiar de verdadero a falso o de falso a verdadero, o si el método llamado se pudiera cambiar para usar el parámetro booleano directamente en lugar de pasarlo. La probabilidad de diez de esas llamadas seguidas es pequeña, pero podría suceder, y si lo hiciera, sería una buena práctica de programación.
El segundo caso es un poco exagerado dado que estamos tratando con un valor booleano. Pero si el programa tiene múltiples hilos o eventos, pasar parámetros es la forma más simple de rastrear datos específicos de hilos / eventos. Por ejemplo, un programa puede recibir información de dos o más sockets. El código que se ejecuta para un socket puede necesitar generar mensajes de advertencia, y uno que se ejecuta para otro no. Entonces (más o menos) tiene sentido que un conjunto booleano en un nivel muy alto pase a través de muchas llamadas de método a los lugares donde se pueden generar mensajes de advertencia. Los datos no se pueden guardar (excepto con gran dificultad) en cualquier tipo de global porque múltiples hilos o eventos intercalados necesitarían cada uno su propio valor.
Sin duda, en este último caso, probablemente crearía una clase / estructura cuyo único contenido era un valor lógico y paso que todo en su lugar. Es casi seguro que necesitaré otros campos en poco tiempo, como por ejemplo dónde enviar los mensajes de advertencia.
fuente
El contexto es importante. Tales métodos son bastante comunes en iOS. Como solo un ejemplo de uso frecuente, UINavigationController proporciona el método
-pushViewController:animated:
y elanimated
parámetro es un BOOL. El método realiza esencialmente la misma función de cualquier manera, pero anima la transición de un controlador de vista a otro si pasa SÍ, y no si pasa NO. Esto parece completamente razonable; Sería una tontería tener que proporcionar dos métodos en lugar de este solo para poder determinar si usar o no la animación.Puede ser más fácil justificar este tipo de cosas en Objective-C, donde la sintaxis de denominación de métodos proporciona más contexto para cada parámetro que el que obtiene en lenguajes como C y Java. Sin embargo, creo que un método que toma un solo parámetro fácilmente podría tomar un valor booleano y seguir teniendo sentido:
fuente
false
significa en elfile.saveWithEncryption
ejemplo. ¿Significa que se guardará sin cifrado? Si es así, ¿por qué el método tendría "con cifrado" justo en el nombre? Podría entender que tiene un método comosave(boolean withEncryption)
, pero cuando veofile.save(false)
, no es del todo obvio de un vistazo que el parámetro indique que sería con o sin cifrado. Creo que, de hecho, este es el primer punto de James Youngman, sobre el uso de una enumeración.false
significa no sobrecargar ningún archivo existente del mismo nombre. Ejemplo contribuido, lo sé, pero para estar seguro, necesitará verificar la documentación (o código) para la función.saveWithEncryption
que a veces no se guarda con cifrado es un error. Posiblemente debería serfile.encrypt().save()
, o como Javasnew EncryptingOutputStream(new FileOutputStream(...)).write(file)
.saveWithEncryption(boolean)
que a veces se guarda sin cifrado, al igual que no vuela para hacer un métodosaveWithoutEncryption(boolean)
que a veces se guarda con cifrado.