Refactorización de conditional if en C#: Evitar el desorden condicional con Derek Comartin
{academy-video-youtube({"vid": "QoK7jSZ-viw", "start_time": "0", "title": "Los Enums no son malos. Los condicionales en todas partes son", "creador": "Derek Comartin", "duración": "11m 03s"})}]
En C#, las sentencias condicionales como la sentencia if, la sentencia if else y la sentencia switch son herramientas esenciales. Pero, ¿qué ocurre cuando estas construcciones se utilizan en exceso, especialmente vinculadas a enums? Derek Comartin, en su vídeo "Enums Aren't Evil. Conditionals Everywhere Are", nos lleva a través de una refactorización detallada que sustituye la lógica condicional generalizada por patrones más limpios y fáciles de mantener.
En este artículo, recorreremos el razonamiento de Derek paso a paso, utilizando sus marcas de tiempo como anclas. También exploraremos cómo sus ideas se aplican a patrones condicionales comunes en C#, como el operador ternario, la sentencia else y las estructuras switch-case, destacando dónde pueden causar problemas en grandes bases de código y cómo se puede refactorizar hacia un mejor diseño.
La explosión de los if condicionales: El verdadero problema
Derek comienza mostrando una sentencia if que comprueba el tipo de producto:
if (productType == ProductType.Template || productType == ProductType.Ebook)
if (productType == ProductType.Template || productType == ProductType.Ebook)
A primera vista, el if condicional anterior parece sencillo. Pero Derek advierte que este tipo de sentencia evalúa una condición dada y luego ejecuta un bloque de código sólo si la condición es verdadera, lo que se vuelve problemático cuando esta lógica se repite en todas partes.
Es posible que vuelva a tener este bloque en otro método o clase:
if (offeringType == ProductType.Template || offeringType == ProductType.Ebook)
if (offeringType == ProductType.Template || offeringType == ProductType.Ebook)
Derek explica que este patrón se extiende rápidamente por un gran sistema. La misma lógica if else aparece en varios servicios, lo que provoca incoherencias y errores al añadir un nuevo valor enum. Por ejemplo, ¿qué ocurre cuando se introduce un nuevo tipo de producto como Video? Tendrás que acordarte de actualizar cada bloque en el que exista esta expresión condicional.
La repetición amplifica la complejidad
En el siguiente ejemplo, Derek profundiza en los condicionales anidados. Dentro de un método, una sentencia if else comprueba el mismo enum, y el resultado se pasa a otro método, que también contiene una comprobación similar.
La sentencia comprueba si es Template o Ebook y devuelve algo; de lo contrario, devuelve null. Derek señala que esta redundancia no solo alarga el código, sino que introduce riesgos de mantenimiento. La misma lógica se copia en varios archivos, lo que provoca un caos en el flujo de control.
Si tu sistema requiere que añadas un caso por defecto cada vez que tocas un enum, sabes que algo va mal.
Cambiando la forma de pensar sobre los condicionales
En lugar de comprobar constantemente los tipos con if else, Derek propone hacer una pregunta mejor:
¿Tiene el producto características descargables?
Se trata de una mejor expresión de intenciones. Hace que su código sea más legible y reduce por completo la dependencia de la enum. En lugar de escribir una sentencia if con dos condiciones como:
if (product.Type == ProductType.Template || product.Type == ProductType.Ebook)
if (product.Type == ProductType.Template || product.Type == ProductType.Ebook)
Basta con encapsular esa lógica en un modelo y escribir:
if (product.HasDownloadableResource())
if (product.HasDownloadableResource())
Esto sólo se cumple cuando existe un recurso descargable, lo que reduce la necesidad de expresiones condicionales complejas.
De las sentencias If al comportamiento encapsulado
Para resolver el problema principal, Derek introduce un tipo de DownloadableResource. Este tipo incluye una URL de descarga y un nombre de archivo predeterminado. Se convierte en una parte de primera clase de su dominio, en lugar de depender de declaraciones if para derivarlo.
Ahora, en lugar de repetir esto
if (product.Type == ProductType.Template)
{
// Generate file name
}
else if (product.Type == ProductType.Ebook)
{
// Generate file name
}
if (product.Type == ProductType.Template)
{
// Generate file name
}
else if (product.Type == ProductType.Ebook)
{
// Generate file name
}
Escribe esto:
var downloadable = product.GetDownloadableResource();
if (downloadable != null)
{
Console.WriteLine(downloadable.FileName);
}
var downloadable = product.GetDownloadableResource();
if (downloadable != null)
{
Console.WriteLine(downloadable.FileName);
}
Esto simplifica drásticamente la lógica y elimina la necesidad de ramificaciones de sentencias else o incluso de sentencias switch.
Tiempo de ejecución frente a tiempo de compilación: un cambio estratégico
Derek va más allá y explica una importante decisión de diseño: trasladar la lógica del tiempo de compilación al tiempo de ejecución. Esto significa consultar el sistema en tiempo de ejecución para ver si existe un DownloadableResource para un producto. Si es así, actúe en consecuencia. Si no es así, sáltatelo.
Señala que este movimiento convierte la lógica estática if else en consultas en tiempo de ejecución. Puede que añada una llamada a la base de datos, pero reduce la lógica if else anidada y centraliza el comportamiento. Esto mejora la capacidad de mantenimiento a escala.
Uso de la herencia para productos descargables
Otra vía que explora Derek es la herencia. Se puede crear una clase base abstracta Product y luego definir tipos derivados como Ebook, Template o OfflineCourse.
Cada uno anula métodos como
public virtual string GetDownloadUrl() { ... }
public virtual string GetDownloadUrl() { ... }
Este enfoque permite que cada producto maneje su propia lógica. Aunque esto evita una sentencia switch o múltiples sentencias condicionales, Derek señala que aún puedes acabar escribiendo expresiones condicionales internamente si no tienes cuidado.
Mejor encapsulación sin herencia
Si la herencia se siente pesada, Derek sugiere el uso de tipos explícitos como DownloadableProduct, que contiene sus propias propiedades y métodos, sin estar atado a una jerarquía.
En su programa, esto podría ser así:
var downloader = new DownloadableProduct(product);
Console.WriteLine(downloader.GetDefaultFileName());
var downloader = new DownloadableProduct(product);
Console.WriteLine(downloader.GetDefaultFileName());
No es necesaria una sentencia if else o switch para determinar el comportamiento: cada objeto sabe lo que tiene que hacer.
Lightweight Fix: Métodos de extensión en Enums
Si no estás listo para abandonar los enums, Derek propone una solución ligera: crear un método de extensión:
public static bool IsDownloadable(this ProductType type)
{
return type == ProductType.Template || type == ProductType.Ebook;
}
public static bool IsDownloadable(this ProductType type)
{
return type == ProductType.Template || type == ProductType.Ebook;
}
Ahora, en lugar de escribir:
if (product.Type == ProductType.Template || product.Type == ProductType.Ebook)
if (product.Type == ProductType.Template || product.Type == ProductType.Ebook)
Se puede simplificar a:
if (product.Type.IsDownloadable())
if (product.Type.IsDownloadable())
Esto centraliza su lógica y evita repetir las llaves y el bloque de código una y otra vez.
Evitar el uso excesivo de operadores ternarios y Switch
Derek también advierte contra el uso excesivo de abreviaturas como el operador ternario:
string filename = product.Type == ProductType.Template ? "template.pdf" : "default.pdf";
string filename = product.Type == ProductType.Template ? "template.pdf" : "default.pdf";
Aunque se trata de una sintaxis válida, puede ser propensa a errores y difícil de leer cuando la lógica se vuelve compleja. Especialmente si la condición se evalúa como falsa, podría asignarse un valor incorrecto de forma sutil.
Del mismo modo, un switch con una sentencia break y un case por defecto también cae en esta trampa. Es mejor preguntar a los objetos por su comportamiento que utilizar la lógica de switch-case.
Conclusión: Control más inteligente con menos desorden condicional
En conclusión, el vídeo de Derek no es un ataque a los enums, sino una crítica de cómo utilizamos las estructuras condicionales if en torno a ellos. Al extender las sentencias if else y switch por toda la base de código, se dificulta la comprobación, el mantenimiento y la evolución del sistema.
Independientemente de que se opte por la encapsulación, la búsqueda en tiempo de ejecución, la herencia o simples métodos de extensión, el objetivo sigue siendo el mismo: reducir los condicionales y trasladar la lógica al lugar que le corresponde.
Recuerde:
-
Los condicionales no están mal.
-
El desorden condicional lo es.
-
El código limpio no se basa en múltiples sentencias if else dispersas por las clases.
- Evalúe su contexto y refactorice en consecuencia.
Como dice Derek: "Depende del contexto" Pero una cosa es cierta: un producto no siempre es solo un producto; a veces, es una señal para replantearse el diseño.
