Crear modelos más ricos sin tipos enumerados

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 tipos enumerados, 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:

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:

public decimal GetMaxAmountToPayWith(PaymentMethod method)
{
  switch (method)
  {
    case PaymentMethod.CreditCard:
      return 1000m;
    case PaymentMethod.PayPal:
      return 100m;
    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:

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 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:

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:

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:

public abstract class PaymentMethod
{
  public static readonly PaymentMethod CreditCard = new CreditCard();
  public static readonly PaymentMethod PayPal = new PayPal();
  public static readonly PaymentMethod BankTransfer = 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("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 double dispatch para interactuar con el servicio 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, LLBLGen se basa en TypeConverters).

De todas formas, siempre se pueden aplicar soluciones “imaginativas” (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.


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

  1. Miguel Ángel Martín Hernández dijo:

    Juanma, buen post y excelente código.
    Gracias por compartirlo.

  2. Una pequeña observación casi sin importancia: la versión enum/switch es *mucho* más flexible que PaymentMethod; el motivo es que el primero no impone ninguna estructura a funcionalidades futuras (incluyendo modificaciones), mientras que el segundo sí.

    Es el precio por tener un modelo que los humanos podamos comprender y manejar.

  3. No me he explicado con claridad, has asumido que ese otro “algo” no lo podemos comprender ni manejar.

    Yo todo lo que he dicho, es que usando OOP (para comprenderlo y manejarlo) pagas un precio, no que no puedas comprender ni manejar soluciones basadas en enum/switch.

    De echo, a mi me atraen más ese otro tipo de soluciones.

    —–
    “Es el precio por tener un modelo que los humanos podamos comprender y manejar.” == “Es el precio de usar OOP.”

  4. Estoy de acuerdo en que aplicando OOP pagas un precio (lo digo al final del post) y que hay otras formas de modelar esto (también lo menciono en el post), pero creo que la flexibilidad no sólo consiste en “imponer menos cosas”.

    Para mi la flexibilidad debe permitirme adaptar el software nuevos requisitos, y a veces tener menos restricciones no hace que sea más fácil hacer esos cambios.

    Conozco algunos (y no miro a nadie) que piensan que el software desarrollado con lenguajes de tipado estático, por ejemplo, qué se yo… Haskell, acaba siendo más fácil de evolucionar que el software desarrollado con lenguajes dinámicos como, por decir algo… Javascript.

    En ese escenario la implementación basada en Haskell sería más restrictiva (impone tipos), pero puede que esas restricciones hagan que sea más fácil razonar sobre el programa y permitan adaptarlo mejor a nuevos requisitos.

    En cualquier caso, son temas interesantes para tratar y divagar un rato (rigidez de implementación y su impacto en la evolución del diseño -algo así como “menos es mas”-, y diseño de modelos OOP vs FP).

  5. Juan, confundes las cosas, en concreto, ser más/menos estricto con ser más/menos flexible, más que nada que es obvio que javascript (y todos los “vale todo”) son más flexibles (dudo que yo haya dicho tal cosa, de ser así, me rectifico ahora mismo).

    Pero ahí no he entrado yo que conste, únicamente enum/switch es más flexible (precisamente por ser menos estricto…).

    ¿Ves?, al final parece que nunca estamos de acuerdo porque sencillamente destaca ese 1% del que no :P

  6. Mira, en ese punto estoy de acuerdo: seguramente estemos más de acuerdo de lo que queremos dar a entender.

    Aun así, no veo tan clara la implicación “menos estricto es más flexible”. La jerarquía de clases (que tiene el claro inconveniente de ser bastante más compleja a priori), puedes seguir manejándola como un switch (bueno, con un if/else if/else…) pero además puedes aprovechar el polimorfismo para añadir comportamiento sin tocar código que ya existe.

  7. “no veo tan clara la implicación “menos estricto es más flexible””

    supongamos un sistema A, si creas otro B tomando A pero añadiendo una restricción (no nula) cualquiera, es obvio que B es estrictamente menos flexible que A (más que nada que A puede hacer todo lo que B pero además aquello en lo que B está restringido).

    Tu estructura de clases añade el código que resuelve el problema concreto (exactamente igual que e/s), pero además está añadiendo una estructura abstracta que clasifica ese código, esa estructura adicional, restringe acciones posteriores en el código (justificas el polimorfismo buscando un ejemplo ¡que entra dentro de tu restricción! y eso es tautología :) ).

    Si ahora vamos a lo práctico, tu argumento es que, como te sientes cómodo con esa estructura y muy incómodo con otras, *para ti*, te da más “”””flexibilidad”””” la versión restringida, ¡pero eso *no es* más flexible!, es otra cosa (¿”productividad subjetiva”?).

    En cuanto a la definición de flexible, creo que las acepciones 3 y 4 de la rae lo dejan claro (http://lema.rae.es/drae/?val=flexible):

    “Que no se sujeta a normas estrictas, a dogmas o a trabas.”
    “Susceptible de cambios o variaciones según las circunstancias o necesidades.”

    Que a ti te resulte más cómodo refactorizar todo un árbol de clases antes que buscar “el par” de sitios donde tocar en un código spaguetti (por poner un ejemplo visual) no quiere decir que venga otro señor (ej. un Haskeller :P ) y él sí sepa donde “tocar”.

    Por tanto, eliminando cualquier elemento subjetivo, creo que queda claro que la versión enum/sw es *más* flexible.

    Pero repito, mi comentario inicial venía a decir que en un sentido OOP estaba de acuerdo en todo, pero que en mi opinión era incorrecto (y peligroso) pensar que esa solución es más flexible.

  8. Buen post y código.

    Saludos

    El link en el que haces referencia para: “(bueno, al menos en parte porque en C# se pueden hacer cosas un poco raras con ellos)”, no funciona.

Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos necesarios están marcados *

*

Puedes usar las siguientes etiquetas y atributos HTML: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>