¿Cómo puedo hacer una llamada con un booleano más claro? Trampa booleana

76

Como se señaló en los comentarios de @benjamin-gruenbaum, esto se llama trampa booleana:

Digamos que tengo una función como esta

UpdateRow(var item, bool externalCall);

y en mi controlador, ese valor para externalCallsiempre será VERDADERO. ¿Cuál es la mejor manera de llamar a esta función? Yo suelo escribir

UpdateRow(item, true);

Pero me pregunto, ¿debería declarar un booleano, solo para indicar qué significa ese valor 'verdadero'? Puedes saberlo mirando la declaración de la función, pero obviamente es más rápido y claro si acabas de ver algo como

bool externalCall = true;
UpdateRow(item, externalCall);

PD: No estoy seguro de si esta pregunta realmente encaja aquí, si no es así, ¿dónde podría obtener más información sobre esto?

PD2: No etiqueté ningún idioma porque pensé que era un problema muy genérico. De todos modos, trabajo con c # y la respuesta aceptada funciona para c #

Mario garcia
fuente
10
Este patrón también se llama trampa booleana
Benjamin Gruenbaum
11
Si está utilizando un lenguaje con soporte para tipos de datos algebraicos, le recomiendo un nuevo adt en lugar de un booleano. data CallType = ExternalCall | InternalCallen haskell por ejemplo.
Filip Haglund
66
Supongo que Enums cumpliría exactamente el mismo propósito; obteniendo nombres para los booleanos, y un poco de tipo de seguridad.
Filip Haglund
2
No estoy de acuerdo con que declarar un booleano para indicar el significado es "obviamente más claro". Con la primera opción, es obvio que siempre pasarás verdadero. Con el segundo, debe verificar (¿dónde se define la variable? ¿Cambia su valor?). Claro, eso no es un problema si las dos líneas están juntas ... pero alguien podría decidir que el lugar perfecto para agregar su código es exactamente entre las dos líneas. ¡Sucede!
AJPerez
Declarar que un booleano no es más limpio, más claro, es solo saltar un paso, es decir, mirar la documentación del método en cuestión. Si conoce la firma, es menos claro agregar una nueva constante.
dentro

Respuestas:

154

No siempre hay una solución perfecta, pero tiene muchas alternativas para elegir:

  • Use argumentos con nombre , si están disponibles en su idioma. Esto funciona muy bien y no tiene inconvenientes particulares. En algunos idiomas, cualquier argumento se puede pasar como un argumento con nombre, por ejemplo updateRow(item, externalCall: true)( C # ) o update_row(item, external_call=True)(Python).

    Su sugerencia de usar una variable separada es una forma de simular argumentos con nombre, pero no tiene los beneficios de seguridad asociados (no hay garantía de que haya usado el nombre de variable correcto para ese argumento).

  • Use funciones distintas para su interfaz pública, con mejores nombres. Esta es otra forma de simular parámetros con nombre, colocando los valores de los parámetros en el nombre.

    Esto es muy legible, pero genera muchas repeticiones para usted, que está escribiendo estas funciones. Tampoco puede lidiar bien con la explosión combinatoria cuando hay múltiples argumentos booleanos. Un inconveniente importante es que los clientes no pueden establecer este valor dinámicamente, sino que deben usar if / else para llamar a la función correcta.

  • Usa una enumeración . El problema con los booleanos es que se llaman "verdadero" y "falso". Entonces, en su lugar, introduzca un tipo con mejores nombres (por ejemplo enum CallType { INTERNAL, EXTERNAL }). Como beneficio adicional, esto aumenta la seguridad de tipo de su programa (si su idioma implementa enumeraciones como tipos distintos). El inconveniente de las enumeraciones es que agregan un tipo a su API visible públicamente. Para funciones puramente internas, esto no importa y las enumeraciones no tienen inconvenientes significativos.

    En idiomas sin enumeraciones, a veces se utilizan cadenas cortas . Esto funciona, e incluso puede ser mejor que los booleanos sin procesar, pero es muy susceptible a los errores tipográficos. La función debería afirmar inmediatamente que el argumento coincide con un conjunto de valores posibles.

Ninguna de estas soluciones tiene un impacto prohibitivo en el rendimiento. Los parámetros y enumeraciones con nombre se pueden resolver completamente en tiempo de compilación (para un lenguaje compilado). El uso de cadenas puede implicar una comparación de cadenas, pero el costo es insignificante para los literales de cadenas pequeñas y la mayoría de los tipos de aplicaciones.

amon
fuente
77
Me acabo de enterar de que existen "argumentos con nombre". Creo que esta es, con mucho, la mejor sugerencia. Si puede trabajar un poco en esa opción (para que otras personas no necesiten buscar en Google), aceptaré esta respuesta. También cualquier pista sobre el rendimiento si puedes ... Gracias
Mario Garcia
66
Una cosa que puede ayudar a aliviar los problemas con cadenas cortas es usar constantes con los valores de cadena. @ MarioGarcia No hay mucho que explicar. Además de lo que se menciona aquí, la mayoría de los detalles dependerán del idioma en particular. ¿Hubo algo en particular que quisieras ver mencionado aquí?
jpmc26
8
En los idiomas en los que estamos hablando de un método y la enumeración se puede abarcar a una clase, generalmente utilizo una enumeración. Es completamente autodocumentado (en la única línea de código que declara el tipo de enumeración, por lo que también es liviano), evita por completo que se llame al método con un booleano desnudo y el impacto en la API pública es insignificante (ya que los identificadores son alcanzados a la clase).
davidbak
44
Otro inconveniente del uso de cadenas en este rol es que no puede verificar su validez en tiempo de compilación, a diferencia de las enumeraciones.
Ruslan
32
enum es el ganador. El hecho de que un parámetro solo pueda tener uno de dos estados posibles, no significa que deba ser automáticamente un valor booleano.
Pete
39

La solución correcta es hacer lo que sugieres, pero empaquetarlo en una mini-fachada:

void updateRowExternally() {
  bool externalCall = true;
  UpdateRow(item, externalCall);
}

La legibilidad triunfa sobre la microoptimización. Puede permitirse la llamada de función adicional, ciertamente mejor de lo que puede permitirse el esfuerzo del desarrollador de tener que buscar la semántica de la bandera booleana incluso una vez.

Kilian Foth
fuente
8
¿Vale realmente esta encapsulación cuando solo llamo a esa función una vez en mi controlador?
Mario Garcia
14
Si. Sí lo es. La razón, como escribí anteriormente, es que el costo (una llamada a la función) es menor que el beneficio (ahorras una cantidad no trivial de tiempo de desarrollador porque nadie tiene que buscar lo que truehace). Valdría la pena incluso si cuesta tanto tiempo de CPU como ahorra tiempo de desarrollador, ya que el tiempo de CPU es mucho más barato.
Kilian Foth
8
Además, cualquier optimizador competente debería poder alinearlo para que no pierda nada al final.
firedraco
33
@KilianFoth Excepto que es una suposición falsa. Un mantenedor no necesita saber lo que hace el segundo parámetro, y por qué es verdad, y casi siempre se relaciona esto con el detalle de lo que estamos tratando de hacer. Ocultar la funcionalidad en funciones diminutas de muerte por mil cortes disminuirá la capacidad de mantenimiento, no la aumentará. Lo he visto a mí mismo, es triste decirlo. La división excesiva en funciones puede ser peor que una gran función de Dios.
Graham
66
Creo que UpdateRow(item, true /*external call*/);sería más limpio, en idiomas que permiten esa sintaxis de comentarios. Bloquear su código con una función adicional solo para evitar escribir un comentario no parece valer la pena para un caso simple. Quizás si hubiera muchos otros argumentos para esta función, y / o algún código circundante + complicado alrededor, comenzaría a atraer más. Pero creo que si está depurando, terminará verificando la función del contenedor para ver qué hace, y tendrá que recordar qué funciones son contenedores delgados alrededor de una API de biblioteca y cuáles tienen algo de lógica.
Peter Cordes
25

Digamos que tengo una función como UpdateRow (elemento var, bool externalCall);

¿Por qué tienes una función como esta?

¿En qué circunstancias lo llamaría con el argumento externalCall establecido en valores diferentes?

Si uno es de, digamos, una aplicación externa de cliente y el otro está dentro del mismo programa (es decir, diferentes módulos de código), entonces diría que debe tener dos métodos diferentes , uno para cada caso, posiblemente incluso definido en distintos Interfaces

Sin embargo, si estaba haciendo la llamada basándose en algún dato tomado de una fuente que no es del programa (por ejemplo, un archivo de configuración o una lectura de base de datos), entonces el método de paso booleano tendría más sentido.

Phill W.
fuente
66
Estoy de acuerdo. Y solo para aclarar el punto que se está haciendo para otros lectores, se puede hacer un análisis de todas las personas que llaman, que pasarán verdadero, falso o una variable. Si no se encuentra ninguno de estos últimos, argumentaría a favor de dos métodos separados (sin booleano) sobre uno con el booleano: es un argumento de YAGNI que el booleano introduce una complejidad no utilizada / innecesaria. Incluso si hay algunas personas que llaman que están pasando variables, todavía podría proporcionar las versiones (booleanas) sin parámetros para usar para las personas que llaman que pasan una constante; es más simple, lo cual es bueno.
Erik Eidt
18

Si bien estoy de acuerdo en que es ideal usar una función de lenguaje para imponer tanto la legibilidad como la seguridad del valor, también puede optar por un enfoque práctico : comentarios en tiempo de llamada. Me gusta:

UpdateRow(item, true /* row is an external call */);

o:

UpdateRow(item, true); // true means call is external

o (correctamente, como lo sugiere Frax):

UpdateRow(item, /* externalCall */true);
obispo
fuente
1
No hay razón para rechazarlo. Esta es una sugerencia perfectamente válida.
Suncat2000
¡Aprecio el <3 @ Suncat2000!
obispo
11
Una variante (probablemente preferible) es simplemente poner el nombre del argumento simple allí, como UpdateRow(item, /* externalCall */ true ). El comentario de oraciones completas es mucho más difícil de analizar, en realidad es en su mayoría ruido (especialmente la segunda variante, que también está muy unida al argumento).
Frax
1
Esta es, con mucho, la respuesta más adecuada. Esto es precisamente para lo que están destinados los comentarios.
Sentinel
99
No soy un gran admirador de comentarios como este. Los comentarios son propensos a volverse obsoletos si no se tocan al refactorizar o realizar otros cambios. Sin mencionar que tan pronto como tengas más de un bool param, esto se vuelve confuso rápidamente.
John V.
11
  1. Puedes 'nombrar' tus bools. A continuación se muestra un ejemplo para un lenguaje OO (donde se puede expresar en la clase que proporciona UpdateRow()), sin embargo, el concepto en sí se puede aplicar en cualquier idioma:

    class Table
    {
    public:
        static const bool WithExternalCall = true;
        static const bool WithoutExternalCall = false;
    

    y en el sitio de la llamada:

    UpdateRow(item, Table::WithExternalCall);
    
  2. Creo que el Artículo # 1 es mejor, pero no obliga al usuario a usar nuevas variables cuando usa la función. Si la seguridad de tipos es importante para usted, puede crear un enumtipo y hacer que UpdateRow()acepte esto en lugar de un bool:

    UpdateRow(var item, ExternalCallAvailability ec);

  3. Puede modificar el nombre de la función de alguna manera que refleje mejor el significado del boolparámetro. No estoy muy seguro pero tal vez:

    UpdateRowWithExternalCall(var item, bool externalCall)

simurg
fuente
8
No me gusta el # 3 como ahora si llamas a la función con externalCall=false, su nombre no tiene absolutamente ningún sentido. El resto de tu respuesta es buena.
Carreras de ligereza en órbita
Convenido. No estaba muy seguro, pero puede ser una opción viable para alguna otra función. I
simurg
@LightnessRacesinOrbit De hecho. Es más seguro hacerlo UpdateRowWithUsuallyExternalButPossiblyInternalCall.
Eric Duminil
@EricDuminil: Spot on 😂
Carreras de ligereza en órbita
10

Otra opción que aún no he leído aquí es: usar un IDE moderno.

Por ejemplo, IntelliJ IDEA imprime el nombre de la variable en el método al que está llamando si está pasando un literal como trueo nullo username + “@company.com. Esto se hace en una fuente pequeña para que no ocupe demasiado espacio en su pantalla y se vea muy diferente del código real.

Todavía no digo que sea una buena idea lanzar booleanos en todas partes. El argumento que dice que lee el código con mucha más frecuencia de la que escribe es a menudo muy fuerte, pero para este caso particular depende en gran medida de la tecnología que usted (y sus colegas) usen para respaldar su trabajo. Con un IDE es mucho menos problema que con, por ejemplo, vim.

Ejemplo modificado de parte de mi configuración de prueba: ingrese la descripción de la imagen aquí

Sebastiaan van den Broek
fuente
55
Esto solo sería cierto si todos en su equipo solo usaran un IDE para leer su código. A pesar de la prevalencia de editores que no son IDE, también tiene herramientas de revisión de código, herramientas de control de fuente, preguntas de desbordamiento de pila, etc., y es de vital importancia que el código siga siendo legible incluso en esos contextos.
Daniel Pryden
@DanielPryden seguro, de ahí mi comentario y el de tus colegas. Es común tener un IDE estándar en las empresas. En cuanto a otras herramientas, diría que es completamente posible que ese tipo de herramientas también lo admitan o lo hagan en el futuro, o que esos argumentos simplemente no se aplican (como para un equipo de programación de dos personas)
Sebastiaan van den Broek
2
@Sebastiaan: No creo estar de acuerdo. Incluso como desarrollador en solitario, leo regularmente mi propio código en Git diffs. Git nunca podrá hacer el mismo tipo de visualización contextual que un IDE puede hacer, ni debería hacerlo. Estoy a favor del uso de un buen IDE para ayudarlo a escribir código, pero nunca debe usar un IDE como excusa para escribir código que no hubiera escrito sin él.
Daniel Pryden
1
@DanielPryden No estoy abogando por escribir código extremadamente descuidado. No es una situación en blanco y negro. Puede haber casos en los que, en el pasado, para una situación específica, estaría un 40% a favor de escribir una función con un valor booleano y un 60% no, y terminó sin hacerlo. Quizás con un buen soporte IDE ahora el saldo es del 55% escribiéndolo así y el 45% no. Y sí, es posible que a veces necesite leerlo fuera del IDE, por lo que no es perfecto. Pero sigue siendo una compensación válida contra una alternativa, como agregar otro método o enumeración para aclarar el código de lo contrario.
Sebastiaan van den Broek
6

¿2 días y nadie mencionó el polimorfismo?

target.UpdateRow(item);

Cuando soy un cliente que desea actualizar una fila con un elemento, no quiero pensar en el nombre de la base de datos, la URL del microservicio, el protocolo utilizado para comunicarse o si una llamada externa es o no va a necesitar ser usado para que esto suceda. Deja de presionarme estos detalles. Simplemente tome mi artículo y vaya a actualizar una fila en algún lugar ya.

Haga eso y se convierte en parte del problema de construcción. Eso se puede resolver de muchas maneras. Aquí hay uno:

Target xyzTargetFactory(TargetBuilder targetBuilder) {
    return targetBuilder
        .connectionString("some connection string")
        .table("table_name")
        .external()
        .build()
    ; 
}

Si estás viendo eso y pensando "Pero mi lenguaje ha llamado argumentos, no necesito esto". Bien, genial, ve a usarlos. Simplemente mantenga esta tontería fuera del cliente que llama que ni siquiera debería saber con qué está hablando.

Cabe señalar que esto es más que un problema semántico. También es un problema de diseño. Pase un booleano y se verá obligado a usar la ramificación para lidiar con él. No muy orientado a objetos. ¿Tienes dos o más booleanos para pasar? ¿Desea que su idioma tenga despacho múltiple? Mira lo que pueden hacer las colecciones cuando las anidas. La estructura de datos correcta puede hacer su vida mucho más fácil.

naranja confitada
fuente
2

Si puede modificar la updateRowfunción, quizás pueda refactorizarla en dos funciones. Dado que la función toma un parámetro booleano, sospecho que se ve así:

function updateRow(var item, bool externalCall) {
  Database.update(item);

  if (externalCall) {
    Service.call();
  }
}

Eso puede ser un poco de olor a código. La función podría tener un comportamiento dramáticamente diferente según la externalCallvariable establecida, en cuyo caso tiene dos responsabilidades diferentes. Refactorizarlo en dos funciones que solo tienen una responsabilidad puede mejorar la legibilidad:

function updateRowAndService(var item) {
  updateRow(item);
  Service.call();
}

function updateRow(var item) {
  Database.update(item);
}

Ahora, en cualquier lugar donde llame a estas funciones, puede saber de un vistazo si se está llamando al servicio externo.

Este no es siempre el caso, por supuesto. Es situacional y una cuestión de gustos. Por lo general, vale la pena considerar la refactorización de una función que toma un parámetro booleano en dos funciones, pero no siempre es la mejor opción.

Kevin
fuente
0

Si UpdateRow está en una base de código controlada, consideraría el patrón de estrategia:

public delegate void Request(string query);    
public void UpdateRow(Item item, Request request);

Donde Solicitud representa algún tipo de DAO (una devolución de llamada en caso trivial).

Caso verdadero:

UpdateRow(item, query =>  queryDatabase(query) ); // perform remote call

Caso falso:

UpdateRow(item, query => readLocalCache(query) ); // skip remote call

Por lo general, la implementación del punto final se configuraría en un nivel superior de abstracción y simplemente la usaría aquí. Como un útil efecto secundario gratuito, esto me da la opción de implementar otra forma de acceder a datos remotos, sin ningún cambio en el código:

UpdateRow(item, query => {
  var data = readLocalCache(query);
  if (data == null) {
    data = queryDatabase(query);
  }
  return data;
} );

En general, dicha inversión de control asegura un menor acoplamiento entre el almacenamiento de datos y el modelo.

Basilevs
fuente