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 ;)

18 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, 21 de enero de 2025
Programadores intentando entender un código imposible

En general, el uso de expresiones largas debería estar prohibido por ley. Hay pocas cosas que ataquen tanto a la legibilidad de un código como una expresión larga y compleja a la que tengamos que dedicar un buen rato cada vez que intentemos comprenderla. Y no digamos si nos toca modificarla 😱

Aunque esto es aplicable a cualquier tipo de expresión, en este post me voy a centrar en los predicados, que al fin y al cabo representan las condiciones que determinan el comportamiento de una aplicación.

Veamos un ejemplo de if con un predicado que implementa una condición de negocio necesaria para que un usuario user pueda realizar una tarea concreta en la cuenta account:

if (account is not null && account.IsActive && account.HasSubscription 
    && account.SubscriptionStartDate <= DateTime.Today && account.SubscriptionEndDate 
    >= DateTime.Now.AddDays(1) && (account.SubscriptionLevel == "Standard" || 
    account.SubscriptionLevel == "Premium") && user.IsActive && (user.HasRole("admin") 
    || (user.HasRole("owner"))) && user.AccountId == account.Id)
{
    account.ChangePaymentMethod("paypal");
}

Es un poco difícil de leer, ¿verdad? 😅 Bueno, está algo forzado para ilustrar bien el ejemplo, pero seguro que en nuestras bases de código podríamos reconocer fragmentos conceptualmente similares.

Básicamente, la condición anterior verifica si el estado de la cuenta account es válido para realizar la tarea, comprobando que sea correcta, esté activa, y tenga en vigor una suscripción de nivel "Standard" o "Premium", así como que el usuario user tenga permisos para hacerlo, comprobando que esté activo, tenga un rol de "admin" u "owner", y pertenezca a la cuenta account.

Obviamente, no podemos evitar que la lógica de negocio sea compleja, eso no depende de nosotros. Pero sí podemos hacer que la forma de expresarla mediante código sea más sencilla y legible, así que, ¡manos a la obra!

Dividir para vencer

Desde siempre, una forma bastante eficaz de simplificar cosas complejas es trocearlas en partes más pequeñas y fáciles de digerir, es decir, aplicar el famoso "divide y vencerás". Hace tiempo vimos por aquí cómo aplicar esta técnica para comprender un código imposible, pero también podemos usarla para mejorar la legibilidad de nuestro código. Troceando los predicados en partes más pequeñas, cada una de estas porciones será una expresión más sencilla de entender y de mantener.

Si analizamos la lógica anterior, podemos identificar dos grupos de condiciones. El primero determina si la cuenta es válida, es decir, si está en un estado apropiado para realizar la operación, y el segundo, si el usuario tiene capacidad para iniciarla.

Por tanto, en una primera iteración podemos dividir el predicado en dos métodos que se encarguen de evaluar cada uno de estos grupos de condiciones. Además, aprovechamos para formatear un poco el código y hacerlo más legible, con lo que quedaría algo así:

// Condición original simplificada:
if(IsValidAccount(account) && IsUserManagerOfAccount(user, account))
{
    account.ChangePaymentMethod("paypal");
}
...

bool IsValidAccount(Account account)
{
    return account is not null 
          && account.IsActive 
          && account.HasSubscription 
          && account.SubscriptionStartDate <= DateTime.Today 
          && account.SubscriptionEndDate >= DateTime.Now.AddDays(1) 
          && (
              account.SubscriptionLevel == "Standard" 
              || account.SubscriptionLevel == "Premium"
          )
}

bool IsUserManagerOfAccount(User user, Account account)
{
    return user.IsActive 
          && (user.HasRole("admin") || user.HasRole("owner")) 
          && user.AccountId == account.Id;
}

Aunque hemos escrito más código, éste es mucho más legible y fácil de mantener, así que nuestro yo del futuro nos agradecerá el esfuerzo. Podremos entender rápidamente qué condiciones se están evaluando y, además, si más adelante necesitamos modificar alguna de las condiciones, lo haremos de forma más segura, ya que cada parte del predicado está encapsulada en una función o método independiente, que incluso podríamos reutilizar en otros lugares de nuestro código o introducir en tests para asegurar su correcto comportamiento a la larga.

Pero aún así, todavía podríamos encontrarnos con escenarios en los que este paso no sea suficiente porque quizás la lógica de cada una de estas porciones sea compleja en sí misma. En ese caso, podríamos seguir troceando cada una de estas partes en fragmentos más pequeños, hasta que cada uno de ellos sea lo suficientemente sencillo de entender.

Echemos un vistazo a la siguiente iteración, donde hemos refactorizado las condiciones usando variables, siempre con un nombre apropiado, que representan cada una de las partes de la lógica de negocio:

// Condición original simplificada:
if(IsValidAccount(account) && IsUserManagerOfAccount(user, account))
{
    account.ChangePaymentMethod("paypal");
}
...

bool IsValidAccount(Account account)
{
    if(account is null) {
        return false; // Quick exit
    }

    bool hasValidSubscription = account.HasSubscription 
              && account.SubscriptionStartDate <= DateTime.Now 
              && account.SubscriptionEndDate => DateTime.Now;
    bool hasCorrectSubscriptionLevel = account.SubscriptionLevel == "Standard" 
              || account.SubscriptionLevel == "Premium";                    

    return account.IsActive && hasValidSubscription && hasCorrectSubscriptionLevel;
}

bool IsUserManagerOfAccount(User user, Account account)
{
    bool userIsActive = user.IsActive;
    bool userHasManagementRole = user.HasRole("admin") || user.HasRole("owner");
    bool userIsInCorrectAccount = user.AccountId == account.Id;
    return userIsActive && userHasManagementRole && userIsInCorrectAccount;
}

Como hemos visto, podemos mejorar considerablemente la legibilidad de nuestros predicados si los troceamos en partes más pequeñas y sencillas. De esta forma, no solo facilitamos su comprensión y mantenimiento, sino que también mejoramos la calidad de nuestro código y, por ende, la de nuestra aplicación.

Sin embargo, hay que tener cuidado con varios detalles. Primero, no debemos abusar de esta técnica, ya que podríamos caer en el extremo opuesto y complicar la lectura del código con un exceso de fragmentación.

También es importante que los nombres de las variables, funciones o métodos usados sean descriptivos y representativos de la lógica que encapsulan, ya que de lo contrario podríamos empeorar la legibilidad del código.

Por último, hay que tener en cuenta que a veces esta refactorización para mejorar la legibilidad convendrá hacerla "in situ", es decir, dentro del mismo método donde se usa el predicado, pero otras veces, la necesidad de realizar las mismas comprobaciones será tan frecuente que valdrá la pena "ascender" su lógica a otros puntos más apropiados. Por ejemplo, la función IsValidAccount quizás tendría sentido implementarla como método IsValid() de la clase Account.

Publicado en Variable not found.

Aún no hay comentarios, ¡sé el primero!