Saltar al contenido

Artículos, tutoriales, trucos, curiosidades, reflexiones y links sobre programación web ASP.NET Core, MVC, Blazor, SignalR, Entity Framework, C#, Azure, Javascript... y lo que venga ;)

17 años online

el blog de José M. Aguilar

Inicio El autor Contactar

Artículos, tutoriales, trucos, curiosidades, reflexiones y links sobre programación web
ASP.NET Core, MVC, Blazor, SignalR, Entity Framework, C#, Azure, Javascript...

¡Microsoft MVP!
martes, 8 de marzo de 2022
Programación No podemos negar que el tipo de datos boolean está fuertemente arraigado en los desarrolladores. Desde nuestros inicios en este mundillo, nos acostumbramos a pensar que las máquinas sólo entienden de unos y ceros o sus variantes humanas: "sí" y "no", "verdadero" y "falso" o similares. Y por alguna misteriosa razón, tendemos a ver la realidad y modelar nuestras soluciones utilizando estas primitivas tan básicas.

Pero aunque indudablemente los tipos booleanos o flags son una fórmula muy compacta para almacenar información, el mundo suele ser mucho más complejo y estas simplificaciones son a menudo origen de problemas y trampas para nuestro yo del futuro.

En este post vamos a ver algunos escenarios en los que este tipo de dato puede llegar a complicarnos la vida.

1. Usar flags a la hora de diseñar entidades de datos es a menudo mala idea

En aplicaciones que van a tener una vida considerable (es decir, básicamente aquellas que no son de usar y tirar), el uso de valores lógicos para modelar entidades o tipos de datos puede resultar bastante limitante, básicamente porque sólo admiten dos valores: cierto o falso. En nuestro mundo, con el tiempo las cosas tienden a complicarse, y lo que en principio eran un par de posibilidades, se convierten fácilmente en varias más.

Por ejemplo, seguro que alguna vez os habéis encontrado con una aplicación (vuestra o de otros) que inicialmente usó una estructura de datos simple para almacenar usuarios, con una propiedad booleana del tipo IsAdmin para distinguir a los administradores del sistema del resto. En este caso, probablemente hayáis comprobado que con el tiempo se van descubriendo nuevos tipos (usuarios estándar, supervisores, administradores, superadministradores...) haciendo que ese booleano que tan cándidamente se eligió al principio para almacenar el dato sea claramente insuficiente.

Otro ejemplo: al comenzar una aplicación sencilla de facturas, podemos tener la tentación de incluir en las facturas un flag para conocer si están pagadas o no.

public class Invoice
{
    ...
    public bool Paid { get; set; }
}

Sin embargo, poco tiempo después, veremos que además de pagadas, las facturas también podrían estar vencidas o en otros estados definidos por el negocio. Y en vez de cambiar la estructura, todavía podríamos tener la mala idea de reflejarlos usando campos bool adicionales:

public class Invoice
{
    ...
    public bool Paid { get; set; }
    public bool Overdue { get; set; }
}

Aquí ya la habremos liado del todo, porque, además de escalar bastante mal y ser un infierno vistas al posterior mantenimiento, podría fácilmente dar lugar a estados inconsistentes. Por ejemplo, a bajo nivel, podría ocurrir que una factura esté vencida y pagada al mismo tiempo, salvo que dediquemos bastante tiempo a controlar casos inválidos.

Probablemente, en enfoque más correcto desde un principio habría sido optar por un tipo enumerado que refleje los distintos estados en los que la factura puede encontrarse en este momento:

public enum InvoiceStatus
{
    Created,
    Pending,
    Overdue,
    Paid,
    ...
}

public class Invoice
{
    ...
    public InvoiceStatus Status { get; set; }
}

Por tanto, creo que la mejor recomendación que se puede dar al respecto es modelar propiedades con flags sólo en los casos en que podamos apostar un brazo a que los valores son y serán exclusivamente true y false. Siempre. Ante la duda, a nivel de código un simple entero o un enumerado es mucho más claro y nos deja la puerta abierta a extensiones futuras.

Otro problema de definir campos con valores booleanos es que a veces éstos llevan asociada información adicional. Por ejemplo, imaginad la siguiente clase User que incluye un campo para marcar que un usuario ha sido eliminado (lo que se suele llamar un "borrado lógico"):

public class User
{
    ... // Otros datos
    public bool IsDeleted { get; set; }
}

Muy probablemente, en poco tiempo podríamos toparnos con la necesidad de conocer cuándo fue eliminado. Obviamente podremos solucionarlo con cierta facilidad añadiendo un campo DeletedAt que se rellene únicamente al borrar el usuario:

public class User
{
    ... // Otros datos
    public bool IsDeleted { get; set; }
    public DateTime? DeletedAt { get; set; }
}

Sin embargo, en este caso existiría una clara redundancia que podría dar lugar a inconsistencias. Sería mejor dejar que sea DeletedAt el único campo responsable de determinar si el usuario está eliminado o no:

public class User
{
    ... // Otros datos
    public DateTime? DeletedAt { get; set; }

    // Y si aún queremos saber de forma sencilla si el usuario fue eliminado
    // o no, siempre podríamos usar una propiedad que lo calcule:
    public bool IsDeleted => DeletedAt.HasValue;
}

2. Enviar parámetros bool a un método suele ser mala idea

Mucho se ha hablado de la oscuridad que introduce el envío de parámetros de tipo booleano. Por ejemplo, echad un vistazo a esto:

var johns = _userRepository.Search("john", true);

A la vista de este código, podemos intuir que el valor "john" es el nombre del usuario que queremos buscar en el repositorio, pero, ¿quiere decir ese true? ¿Que lo obtenga sólo si el usuario está activo? ¿Quizás que se retornen los usuarios con ese nombre y que sean administradores del sistema? ¿O que la búsqueda se realice usando índices mágicos que consigan obtenerlos más rápidamente?

Ciertamente, en lenguajes como C# podemos usar named arguments y la cosa mejora bastante, como podemos ver a continuación, donde se despejan las dudas que teníamos antes; y, por supuesto, también ayuda el correcto nombrado de variables:

var adminJohns = _userRepository.Search("john", isAdmin: true);

Sin embargo, a efectos de facilitar la lectura, seguro resultaría más apropiado y fácilmente extensible algo como lo siguiente:

var adminJohns = _userRepository.Search("john", UserType.Administrator);
var standardJohns = _userRepository.Search("john", UserType.Standard);

Pero aparte de todo esto, un parámetro de tipo boolean suele indicar que la función hace más de una cosa, o tiene baja cohesión. De hecho, suele decirse que se trata de un code smell que nos pide a gritos una refactorización en dos métodos. Un ejemplo simplificado, y quizás algo exagerado, podría ser el siguiente, donde tenemos una función que hace una u otra cosa en función de un valor lógico:

int Calculate(int a, int b, bool add)
{
    if(add)
    {
        return a + b;
    }
    return a - b;
}

Este código rompe el Principio de Responsabilidad Única (SRP), pues la función tiene dos misiones distintas: sumar y restar números. En estos casos, se considera una buena práctica refactorizar en dos métodos distintos, cuya intencionalidad quede clara desde el principio:

public int Add(int a, int b) => a + b;
public int Subtract(int a, int b) => a - b;

Pero cuidado, que la baja cohesión no es propia de los parámetros bool, puede aparecer también utilizando tipos enumerados u otros, si los usamos para bifurcar hacia la ejecución de distintas tareas en función sus valores. Por ejemplo, en el siguiente código del método SetInvoiceStatus() tenemos el mismo problema que hemos visto más arriba, aunque no utilicemos bool:

void SetInvoiceStatus(int invoiceId, InvoiceStatus newStatus)
{
    if(newStatus == InvoiceStatus.Paid) 
    {
        ... // Pasar la factura a pagada
    } 
    else if(newStatus == InvoiceStatus.Pending)
    {
        ... // Pasar la factura a pendiente
    }
    else if(newStatus == InvoiceStatus.Overdue)
    {
        ... // Pasar la factura a vencida
    }
    ...
}

De la misma forma, para no romper el SRP, podría ser recomendable crear métodos como SetInvoiceAsPaid(), SetInvoiceAsPending(), etc.

3. Retornar booleanos suele ser mala idea

Por simplificar, seguro que muchas veces habéis implementado métodos o funciones que realizan alguna acción y retornan un valor booleano para indicar su éxito o fracaso. Es otro de los puntos donde nuestras expectativas iniciales suelen quedarse cortas.

Con el tiempo, muchas veces veremos que un retorno de tipo bool en un método o función es insuficiente para devolver al consumidor información adicional sobre el éxito o fracaso de la operación. Por ejemplo, si se trata de un método que crea un objeto en una base de datos, es habitual querer retornar el identificador autonumérico generado, o si se produce un error nos interesará devolver información sobre el mismo.

Una forma más apropiada de hacerlo sería utilizar clases o estructuras específicas para ello, como en el siguiente ejemplo, donde utilizamos un tipo CreateInvoiceResult para retornar qué tal fue la ejecución del método CreateInvoice():

public CreateInvoiceResult CreateInvoice(Invoice invoice)
{
    try
    {
        ... // Create the invoice
        return CreateInvoiceResult.Ok(newId);
    }
    catch (Exception ex)
    {
        ... // Log & other tasks
        return CreateInvoiceResult.Error(100, $"Error creating invoice {ex}");
    }
}

La clase CreateInvoiceResult podría ser parecida a la siguiente, aunque eso ya es cuestión de gustos:

public class CreateInvoiceResult
{
    public int ErrorCode { get; private init; }
    public string? ErrorMessage { get; private init; }
    public int? CreatedInvoiceId { get; private init; }

    private CreateInvoiceResult() { }
public bool Success => ErrorMessage == null && ErrorCode == 0 && CreatedInvoiceId != 0; public static CreateInvoiceResult Error(int errorCode, string errorMessage) { return new CreateInvoiceResult() { ErrorCode = errorCode, ErrorMessage = errorMessage }; } public static CreateInvoiceResult Ok(int id) { return new CreateInvoiceResult() { CreatedInvoiceId = id }; } }

Conclusión

En este post hemos visto algunos ejemplos de que los tipos bool no son siempre la mejor opción. Sin embargo, esto no quiere decir que debamos descartar los booleans para siempre. Es decir, la propia respuesta a la pregunta con la que encabezábamos el post no tiene una respuesta booleana ;) 

Como siempre, hay ocasiones en las que usar booleans es una opción más que razonable, y no tiene sentido que los descartemos per se y en todos los escenarios. Hay funciones donde el paso de un valor como true o false no deja lugar a dudas respecto a su intencionalidad, ni atenta contra principios o buenas prácticas. Eso ocurrirá cuando el contexto y el nombre del método o función hayan sido elegidos con buen criterio, como en el siguiente ejemplo:

domElement.SetVisible(true);

También hay ocasiones en las que debe usarse indiscutiblemente un retorno del tipo verdadero o falso. Por ejemplo, en la función esNumeroPar(n) podríamos usar un bool como tipo de retorno sin ningún miramiento, porque el resultado no puede ser otra cosa.

En definitiva, lo más recomendable es simplemente que cuando nos veamos creando un parámetro, campo o propiedad de este tipo, nos paremos un momento a pensar dos veces si es realmente lo que necesitamos, tanto en términos de diseño como de legibilidad y mantenibilidad del código.

Si os interesa el tema y queréis leer más sobre ello, aquí tenéis algunos enlaces:

Publicado en Variable not found.

3 Comentarios:

Joseba dijo...

Instructivo. Gracias.

José María Aguilar dijo...

Muchas gracias por el comentario, @Joseba!

Asier Sánchez dijo...

Está genial! Super identificado. Pasa mucho mucho este tipo de cosas, sobre todo lo de devolver respuesta al cliente... siempre acaba siendo una clase y no un simple bool jejeje