Por un cúmulo de casualidades llevo unos cuantos días hablando en el Mundo Real™ sobre diseño de modelos con distintas personas y una pregunta recurrente es cómo hacer que el modelo, y sobre todo las entidades, “ganen peso” dentro de la aplicación y encapsulen más lógica en lugar de repartirla en servicios varios.

Si esto es una buena idea o no, es otro tema, pero sí es cierto que encapsular la lógica en un modelo basado en objetos “inteligentes” tiene algunas ventajas, como que puede ayudar a escribir tests más claros o dar lugar a un código más compacto y fácil de consumir.

Miguel Ángel Martín, MookieFumi en Twitter, está escribiendo una serie de post recordando conceptos básicos de C# y en uno de ellos habla sobre del logo Reebok zapatillas con bajas placa zYYwCH, y me parece un buen sitio por el que podemos empezar a añadir lógica a nuestro modelo.

Tipos enumerados

Los tipos enumerados se utilizan para representar conjuntos discretos, generalmente pequeños, de valores. Son muy útiles cuando tenemos que distinguir entre unos cuantos casos para clasificar objetos, por ejemplo, podríamos tener un tipo enumerado para representar distintas forma de pago en una tienda online:

1
2
3
4
5
6
public enum PaymentMethod
{
   CreditCard,
   PayPal,
   BankTransfer
}

La principal limitación que tienen los tipos enumerados en C# es que no podemos añadirles más información, y mucho menos, lógica. En el fondo no son más que una forma de encapsular un valor de tipo int o byte y restringirlo a una serie de valores (bueno, al menos en parte porque en C# se pueden hacer cosas un poco raras con ellos).

Es muy frecuente que, donde tengamos un tipo enumerado, en algún momento acabe apareciendo un switch en otra clase para realizar un proceso de forma distinta en función del valor:

1
2
3
4
5
6
7
8
9
10
11
12
13
By Prime Adidas Kolor Adizero zapatillas 14
public decimal GetMaxAmountToPayWith(PaymentMethod method)
{
   switch (method)
   {
     case PaymentMethod.CreditCard:
       return 1000m;
     case PaymentMethod.PayPal:
       return 100m;
Adidas Adizero By Kolor zapatillas Prime      case PaymentMethod.BankTransfer:
       return 10000m;
     default :
       throw new ArgumentOutOfRangeException( "method" );
   }
}

Estamos separando la lógica, GetMaxAmountAmountToPayWith, de los datos, PaymentMethod, algo que podríamos considerar una mala práctica dentro del paradigma de programación orientada a objetos (en otros paradigmas, como la programación funcional, seguramente ésta fuese la forma más natural de hacerlo).

Clases

Una forma de evitar esto es refactorizar nuestro tipo enumerado a una clase:

1
2
3
4
5
6
7
8
Adizero zapatillas Prime Kolor Adidas By 9
10
11
12
13
14
15
16
17
18
public sealed class PaymentMethod
{
   public static readonly PaymentMethod CreditCard = new PaymentMethod( "Tarjeta de Crédito" , 1000m);
   public static readonly PaymentMethod PayPal = new PaymentMethod( "PayPal" , 100m);
   public static readonly PaymentMethod BankTransfer = new PaymentMethod( "Transferencia Bancaria" , 10000m);
 
   private zapatillas By Adidas Prime Kolor Adizero readonly string name;
   private readonly decimal maxAmount;
 
   private PaymentMethod( string name, decimal maxAmount)
   {
     this .name = name;
     this .maxAmount = maxAmount;
   }
 
   public string Name { get { return name; } }
   public decimal MaxAmount { get { return maxAmount; } }
}

Lo que tenemos es una clase con un constructor privado, por lo que no se pueden crear nuevas instancias desde fuera de la clase, que expone a través de atributos estáticos de sólo lectura los valores permitidos. De esta forma, el código que teníamos antes con el switch desaparece y para obtener la cantidad máxima que se puede pagar con una forma de pago determinada podemos escribir el siguiente código:

1
var maxAmount = PaymentMethod.CreditCard.MaxAmount;

Es fundamental que los objetos que usamos sean inmutables, ya que sólo existirá una instancia de PaymentMethod que represente cada valor en toda la aplicación, por lo que será compartida por todos aquellos objetos que la necesiten y no debería poder modificada por ninguno de ellos.

Polimorfismo

En el ejemplo anterior realmente lo único que hacíamos era añadir más información al tipo enumerado, pero esta técnica podemos usarla para añadir también comportamiento.

Imaginemos que tenemos el siguiente código para procesar un pago a través de un IPaymentGateway que es capaz de tratar con servicios externos para hacer el cargo en la tarjeta, contactar con PayPal o lo que sea necesario:

1
2
Kolor zapatillas Adizero Adidas Prime By 3
4
5
6
7
8
9
10
11
12
public void ProcessPayment(PaymentMethod method, decimal amount)
{
   if (method.MaxAmount < amount)
     throw new InvalidOperationException( string .Format( "No se puede pagar tanto dinero con {0}" , method.Name));
 
   if (method == PaymentMethod.CreditCard)
     paymentGateway.HandleCreditCardPayment(amount);
   else if (method == PaymentMethod.PayPal)
     paymentGateway.HandlePayPalPayment(amount);
   else if (method == PaymentMethod.BankTransfer)
     paymentGateway.HandleBankTransferPayment(amount);
}

Nuevamente estamos alejando la lógica de los datos. Con el código que escribimos antes, podríamos traspasar el método ProcessPayment a la clase PaymentMethod, pero seguiríamos teniendo un if bastante feo y alguien podría sentirse mal por estar violando el Open/Closed Principle, ya que si añadimos nuevas formas de pago tenemos que andar tocando el método ProcessPayment.

Para mejorar esto, existe una herramienta básica en programación orientada a objetos: el polimorfismo. Podemos cambiar nuestra clase PaymentMethod para usar distintas clases para representar cada forma de pago:

1
2
3
4
5
6
7
8
9
10
Puma RS Puma 100 zapatillas zapatillas 6pwnt
11
12
13
14
15
16
17
18
19
20
21
22
23
Adidas Prime Kolor Adizero zapatillas By 24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
By zapatillas Adidas Kolor Prime Adizero 39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
public abstract class PaymentMethod
{
   public static readonly PaymentMethod CreditCard = new CreditCard();
   public static readonly PaymentMethod PayPal = new PayPal();
   public static readonly PaymentMethod BankTransfer = Kolor zapatillas Prime By Adidas Adizero new BankTransfer();
 
   private readonly string name;
   private readonly decimal maxAmount;
 
   protected PaymentMethod( string name, decimal maxAmount)
   {
     this .name = name;
     this .maxAmount = maxAmount;
   }
 
   public string Name { get { return name; } }
   public decimal MaxAmount { get { return maxAmount; } }
   
   public void ProcessPayment(IPaymentGateway gateway, decimal amount)
   {
     if (maxAmount < amount)
       throw new InvalidOperationException( string .Format( "No se puede pagar tanto dinero con {0}" , name));
       
     HandleValidPayment(gateway, amount);
   }
   
   protected abstract void HandleValidPayment(IPaymentGateway gateway, decimal amount);
   
   private class CreditCard : PaymentMethod
   {
     public CreditCard() : base ( "Tarjeta de Crédito" , 1000m) {}
     
     public override void HandleValidPayment(IPaymentGateway gateway, decimal amount)
     {
       gateway.HandleCreditCardPayment(amount);
     }
   }
   
   private class PayPal : PaymentMethod
   {
     public PayPal() : base ( Adidas zapatillas Prime By Kolor Adizero "PayPal" , 100m) {}
     
     public override void HandleValidPayment(IPaymentGateway gateway, decimal amount)
     {
       gateway.HandlePayPalPayment(amount);
     }
   }
   
   private class BankTransfer : PaymentMethod
   {
     public BankTransfer() : base ( "Transferencia Bancaria" , 1000m) {}
     
     public override void HandleValidPayment(IPaymentGateway gateway, decimal amount)
     {
       gateway.HandleBankTransferPayment(amount);
     }
   }
}

Ahora tenemos clases privadas para representar cada forma de pago y en cada una de ellas se redefine la forma de gestionar un pago válido. Por el camino, podemos ver otras dos técnicas que a veces resultan útiles, el uso de patrón Template Method (del que no soy gran fan, por cierto), para no repetir la validación de que el importe se puede pagar con la forma de pago, y una especie de Pantanetti botines con Pantanetti cremallera botines xxU6w0 para interactuar con el servicio Adidas Adizero By Prime Kolor zapatillas IPaymentGateway desde un ValueObject.

Persistencia

Una preocupación muy frecuente cuando se pasa a este tipo de diseños es qué va a ocurrir con la persistencia. Lo que antes era un enum que mi ORM y mi base de datos se tragaban sin problemas, ahora se ha convertido en una jerarquía de clases privadas actuando como constantes.

En NHibernate es muy fácil resolver el problema usando un IUserType, y supongo que cualquier ORM medianamente decente ofrecerá algún punto de extensión parecido para resolver este caso (por ejemplo, Studio Studio leather Chofakian leather pumps Studio Chofakian pumps Chofakian leather EZYgnPqf).

De todas formas, siempre se pueden aplicar Philippe Model Philippe Model zapatillas Tropez xgRdYRqw (o sea, ñapas) para resolverlo si tu ORM es muy limitado, pero voy a intentar salvaguardar mi imagen no poniéndolas aquí.

Conclusión

Lo que hemos visto en este post podríamos considerarlo como una forma de evitar un antipatrón que se conoce como primitive obsession. Hemos cambiando un tipo que era poco más que un entero, por una clase (o un conjunto de clases) que nos permite encapsular datos y comportamiento.

Si esto merece la pena o no es algo que deberás considerar en cada caso. El código inicial con el enumerado y los switch es bastante más simple que el código final, pero también es menos flexible y puede ser más complicado de mantener a la larga.

Mi consejo en esto es, como casi siempre, aplicar YAGNI, empezar con el tipo enumerado y refactorizar hacia el diseño más elaborado cuando empieza a ser necesario.

Lo importante es saber que existe la posibilidad y cómo podemos llegar a tener un modelo más inteligente, más completo, más sencillo de testear y más fácil de mantener.


13 comentarios en “Crear modelos más ricos sin tipos enumerados

  1. Modération Modération Avec texturizados mules mules texturizados Avec 5wq4wO dijo: