Ahora veamos un ejemplo más completo. Tendremos una clase de reservación de vuelos. Esta contiene varias toma de decisiones dependiendo de la cantidad de puntos del pasajero.
Vemos al código:
using System;
namespace CleanCode.Conditionals
{
public class Passenger
{
public int LoyaltyPoints { get; set; }
}
public class FlyReservation
{
public FlyReservation(Passenger passenger, DateTime dateTime)
{
From = dateTime;
Passenger = passenger;
}
public DateTime From { get; set; }
public Passenger Passenger { get; set; }
public bool IsCanceled { get; set; }
public void Cancel()
{
// Los pasajeros con mas de 100 pueden cancelar antes de las 24 horas
if (Passenger.LoyaltyPoints > 100)
{
// Si la reserva ya comenzó, lanzar una excepción
if (DateTime.Now > From)
{
throw new InvalidOperationException("Es tarde para caneclar.");
}
if ((From - DateTime.Now).TotalHours < 24)
{
throw new InvalidOperationException("Es tarde para caneclar.");
}
IsCanceled = true;
}
else
{
// Los clientes habituales pueden cancelar hasta 48 horas antes
// Si la reserva ya comenzó, lanza una excepción
if (DateTime.Now > From)
{
throw new InvalidOperationException("Es tarde para caneclar.");
}
if ((From - DateTime.Now).TotalHours < 48)
{
throw new InvalidOperationException("Es tarde para caneclar.");
}
IsCanceled = true;
}
}
}
}
Tenemos una clase Passenger con una propiedad LoyaltyPoints, luego, una clase que se llama FlyReservation. Esta última, tiene una propiedad llamada IsCanceled, más un método Cancel donde tenemos varias condiciones correspondientes a reglas de negocio. Estas son las que repararemos.
En la primera parte, Passenger. LoyaltyPoint > 100, dentro podemos ver que se valida si la fecha de hoy no es mayor al From, si lo es, devolverá una excepción “Es tarde para cancelar.”. Luego, tenemos otra validación, si from es menor a 24 horas también lanzará la misma excepción. Si no ocurre nada IsCanceled será true.
En la segunda parte el if tenemos algo similar pero podrá cancelar en menos de 48hs.
Algo para tener presente, cada vez que debamos refactorizar será útil tener las pruebas de unidad correspondientes. Con las pruebas podremos garantizar y validar que nuestro código no se dañe mientras estamos realizando los cambios. Hacer refactoring sin pruebas unitarias es muy peligroso.
Volvamos a nuestro código de ejemplo. Si vemos el bloque:
if (DateTime.Now > From)
{
throw new InvalidOperationException("Es tarde para cancelar.");
}
Este bloque se repite en 2 lugares. Podemos reducirlo a:
if (DateTime.Now > From)
throw new InvalidOperationException("Es tarde para cancelar.");
Lo pondremos al principio porque es una validación general:
public void Cancel()
{
if (DateTime.Now > From)
throw new InvalidOperationException("Es tarde para cancelar.");
// Los pasajeros con mas de 100 pueden cancelar antes de las 24 horas
if (Passenger.LoyaltyPoints > 100)
{
...
El siguiente arreglo tiene que ver con la segunda excepción, son bastante parecidas, excepto por el valor de evaluación. Este valor es un Magic Number. Vamos a ponerlo en un método llamado LessThan para que sea más comprensible.
private bool LessThan(int hours){
return (From - DateTime.Now).TotalHours < hours;
}
Ahora modificaremos nuestros bloques con el nuevo método:
if (Passenger.LoyaltyPoints > 100)
{
if (LessThan(24))
{
throw new InvalidOperationException("Es tarde para cancelar.");
}
IsCanceled = true;
}
else
{
if (LessThan(48))
{
throw new InvalidOperationException("Es tarde para cancelar.");
}
IsCanceled = true;
}
Lo siguiente será extraer en un método Customer Passenger. LoyaltyPoints > 100. El objetivo es eliminar el comentario innecesario. Lo llamaremos IsGoldPassenger():
if (IsGoldPassenger())
{
if (LessThan(24))
{
....
Ahora podemos ver que tenemos dos bloques, que son bastante similares. Donde tenemos las invocaciones a los métodos LessThan. En el primero podemos ver que tenemos la LessThan(24) y debajo LessThan(48). Podemos eliminarlos y cambiarlos para tener una bifurcación menos de la siguiente manera.
public void Cancel()
{
if (DateTime.Now > From)
throw new InvalidOperationException("Es tarde para cancelar.");
if (IsGoldPassenger() && LessThan(24))
{
throw new InvalidOperationException("Es tarde para cancelar.");
}
if (IsGoldPassenger() &&LessThan(48))
{
throw new InvalidOperationException("Es tarde para cancelar.");
}
IsCanceled = true;
}
Ahora vemos que nuevamente nos quedan muy similares, tenemos 2 excepciones. vamos a eliminar una unión de los 2 if con operadores lógicos para hacerlo más expresivo.
public void Cancel()
{
if (DateTime.Now > From)
throw new InvalidOperationException("Es tarde para cancelar.");
if ((IsGoldPassenger() && LessThan(24)) || !(IsGoldPassenger() &&LessThan(48)))
{
throw new InvalidOperationException("Es tarde para cancelar.");
}
IsCanceled = true;
}
Si lo pensamos un poco más, puede ser más expresivo aún. ¿Como? podemos extraerlo en un método. Lo llamaremos IsCancellationPeriodOver y no devolverá solamente un valor boolean.
public void Cancel()
{
if (DateTime.Now > From)
throw new InvalidOperationException("Es tarde para cancelar.");
if (IsCancellationPreriodOver())
{
throw new InvalidOperationException("Es tarde para cancelar.");
}
IsCanceled = true;
}
private bool IsCancellationPreriodOver(){
return (IsGoldPassenger() && LessThan(24)) || !(IsGoldPassenger() &&LessThan(48));
}
Lo mismo haremos con la comparación de DateTime.Now > from, lo sacaremos a un método que llamaremos IsAlreadyStarted().
public void Cancel()
{
if ( IsAlreadyStarted())
throw new InvalidOperationException("Es tarde para cancelar.");
if (IsCancellationPreriodOver())
{
throw new InvalidOperationException("Es tarde para cancelar.");
}
IsCanceled = true;
}
private bool IsAlreadyStarted(){
return DateTime.Now > From;
}
Una vez más, vemos que nos queda la excepción repetida. Podemos eliminarla nuevamente con un operador lógico uniendolas en un solo if.
public void Cancel()
{
if ( IsAlreadyStarted() ||IsCancellationPreriodOver() )
throw new InvalidOperationException("Es tarde para cancelar.");
IsCanceled = true;
}
Pensando detenidamente, el último método que agregamos, es redundante. Podríamos quitarlo sin problemas.
Por último, el método IsGoldPassenger, en realidad no debe pertenecer a la clase FlyReservation, este método pertenece a passenger. Si lo dejamos dentro de FlyReservation estamos rompiendo el principio de que cada clase debe ser experta en lo que hace, y en este caso, no es la especialidad de FlyReservation. Por esta razón, IsGoldPassenger, debemos moverlo a la clase Customers.
public class Passenger
{
public int LoyaltyPoints { get; set; }
public bool IsGoldPassenger(){
return Passenger.LoyaltyPoints > 100;
}
}
Y por último modificar el método IsCAncellationPeriodOver() para que tome es método de la clase passenger.
private bool IsCancellationPreriodOver(){
return (Passenger.IsGoldPassenger && LessThan(24)) || !(Passenger.IsGoldPassenger &&LessThan(48));
}
Conclusión
Si miramos vemos nuestra clase FlyReservation, nuestro método de cancelación, quedó solamente de 3 líneas. Habíamos comenzado con 6 bifurcaciones y logramos convertirla en una sola. Hicimos el código mucho más descriptivo y podríamos leerlo como si fuera un diario.
En el próximo post veremos Swicth, otra de las bifurcaciones que pueden traernos algunos problemas.