Tecnología de vanguardia

Diseño de un modelo de dominio

Dino Esposito

 

Dino EspositoLa última versión de Entity Framework 4.1 y el nuevo patrón de desarrollo Code First transgreden una de las reglas fundamentales del desarrollo para servidores: no dar ni un solo paso si la base de datos no está lista. Con el patrón Code First los desarrolladores se concentran en el dominio de negocio y en modelarlo en términos de clases. En cierto modo, Code First incentiva aplicar los principios del Diseño guiado por el dominio (DDD) en el espacio .NET. Los dominios de negocio se rellenan con entidades relacionadas e interconectadas; cada una expone sus propios datos en forma de propiedades y puede exponer el comportamiento mediante métodos y eventos. Lo que es más importante, cada entidad puede tener un estado y depender de una lista (posiblemente dinámica) de reglas de validación.

Escribir un modelo de objeto para un escenario real genera ciertos problemas que no se tratan en los tutoriales ni en las demostraciones. En este artículo aceptaré el reto y abordaré la construcción de una clase Customer para representar clientes. Tocaré varios patrones y prácticas de diseño sobre la marcha, tales como el patrón Party, las raíces agregadas, fábricas y tecnologías como los Code Contracts y Validation Application Block (VAB) de Enterprise Library.

Como referencia, le recomiendo que eche un vistazo a este proyectos de código abierto del cual solo alcanzaremos a examinar un pequeño subconjunto del código. Creado por Andrea Saltarello, el proyecto Northwind Starter Kit (nsk.codeplex.com) pretende ilustrar algunas prácticas eficaces para la arquitectura de soluciones en varias capas.

Modelo de objetos frente al modelo de dominio

El debate sobre si emplear un modelo de objetos o un modelo de dominio puede parecer fútil, y en gran medida no es más que un asunto de terminologías. Pero una terminología precisa es un factor clave para garantizar que todos los miembros de un equipo piensen en el mismo concepto cuando emplean un término específico.

En la industria del software prácticamente todos concuerdan en que un modelo de objetos es una colección de objetos genéricos pero posiblemente relacionados. ¿Y en qué difieren los modelos de dominio? A la larga, los modelos de dominio siguen siendo modelos de objetos, y por lo tanto quizás no sea un error tan grave si los dos términos se usan indistintamente. Aun así, cuando el término “modelo de dominio” se emplea con determinado énfasis, es posible que conlleve ciertas expectativas acerca de las formas de los objetos que lo constituyen.

Este uso del modelo de dominio se asocia con la definición dada por Martin Fowler: un modelo de objetos del dominio que incorpora comportamiento y datos. El comportamiento, a su vez, expresa las reglas y la lógica específica (consulte bit.ly/6Ol6uQ).

El diseño guiado por el dominio añade unas cuantas reglas pragmáticas al modelo de dominio. Según este enfoque, un modelo de dominio difiere de un modelo de objetos en la recomendación de usar extensamente objetos de valor en vez de los tipos primitivos. Un entero, por ejemplo, puede representar muchas cosas: una temperatura, una suma de dinero, un tamaño, una cantidad. Los modelos de dominio emplearían un objeto de valor específico para cada situación diferente.

Además, los modelos de dominio deberían identificar las raíces agregadas. Una raíz agregada es una entidad que se obtiene al agregar otras entidades. Los objetos de la raíz agregada no tienen ninguna importancia fuera de ella; es decir, no se usan a menos que provengan del objeto raíz. El ejemplo canónico de una raíz agregada es la entidad Order, pedido. Order contiene un OrderItem agregado para representar cada pedido, pero ningún Product. Cuesta imaginar (aunque esto solo lo puede determinar las especificaciones) que haya que trabajar con un OrderItem que no provenga de un Order. Por otro lado, es perfectamente probable que existan casos en que haya que trabajar con entidades Product que no impliquen ningún pedido. Las raíces agregadas son responsables de mantener sus objetos secundarios en un estado válido y de persistirlos.

Por último, algunas clases de modelos de dominio pueden ofrecer métodos de fábrica públicos en vez de constructores para la creación de nuevas instancias. Cuando la clase es mayoritariamente independiente y no forma parte de ninguna jerarquía, o cuando los pasos que crean la clase tienen alguna importancia para el cliente, entonces el uso de un constructor llano es aceptable. En el caso de los objetos complejos como las raíces agregadas, sin embargo, la creación de instancias requiere de un nivel de abstracción adicional. DDD emplea objetos de fábrica (o más sencillo, métodos de fábrica en algunas clases) como una forma para desacoplar los requisitos del cliente de los objetos internos y sus relaciones y reglas. En bit.ly/oxoJD9 podrá encontrar una introducción muy clara y concisa a DDD.

El patrón Party

Concentrémonos en una clase Customer. A la luz de lo que mencioné previamente, esta sería una firma posible:

public class Customer : Organization, IAggregateRoot
{
  ...
}

¿Pero quién es el cliente? ¿Es una persona, una empresa, o ambos? El patrón Party sugiere hacer la distinción entre ambos y defina claramente qué propiedades son compartidas y cuáles pertenecen solo a las personas o solo a las empresas. El código de la figura 1 se limita a las clases Person y Organization; si el dominio de negocio así lo exige, el esquema se podría detallar aún más al dividir las organizaciones en las empresas comerciales y las organizaciones sin fines de lucro.

Figura 1 Clases según el patrón Party

public abstract class Party
{
  public virtual String Name { get; set; }
  public virtual PostalAddress MainPostalAddress { get; set; }
}
public abstract class Person : Party
{
  public virtual String Surname { get; set; }
  public virtual DateTime BirthDate { get; set; }
  public virtual String Ssn { get; set; }
}
public abstract class Organization : Party
{
  public virtual String VatId { get; set; }
}

Conviene siempre recordar que hay que intentar producir un modelo que se asemeje estrechamente al dominio de negocio real, en vez de una representación abstracta del negocio. Si los requisitos solo hablan de los clientes como personas, entonces el uso del patrón Party no es estrictamente necesario, aunque podría servir como punto de extensión para el futuro. 

Customer como una clase de raíz agregada

Una raíz agregada es una clase dentro del modelo que representa una entidad independiente, una que no existe en relación con ninguna otra entidad. La mayoría de las veces, las raíces agregadas no son más que clases individuales que no administran ningún objeto secundario o simplemente apuntan a la raíz de otros agregados. La figura 2 muestra un poco más de la clase Customer.

Figura 2 La clase Customer como una clase raíz agregada

public class Customer : Organization, IAggregateRoot
{
  public static Customer CreateNewCustomer(
    String id, String companyName, String contactName)
  {
    ...
  }
 
  protected Customer()
  {
  }
 
  public virtual String Id { get; set; }
    ...
 
  public virtual IEnumerable<Order> Orders
  {
    get { return _Orders; }
  }
   
  Boolean IAggregateRoot.CanBeSaved
  {
    get { return IsValidForRegistration; }
  }
 
  Boolean IAggregateRoot.CanBeDeleted
  {
    get { return true; }
  }
}

Como podrá observar, la clase Customer implementa la interfaz (personalizada) IAggregateRoot. Esta es la interfaz:

public interface IAggregateRoot
{
  Boolean CanBeSaved { get; }
  Boolean CanBeDeleted { get; }
}

¿Qué significa ser una raíz agregada? Una raíz agregada es responsable de la persistencia de sus objetos agregados secundarios y se encarga de hacer cumplir las condiciones invariantes que forman parte del grupo. Resulta ser que las raíces agregadas deben ser capaces de comprobar si es posible guardar o eliminar la pila en su totalidad. Una raíz agregada independiente, en cambio, simplemente devuelve verdadero sin realizar ningún tipo de pruebas.

Fábricas y constructores

Los constructores son específicos para cada tipo. Cuando el objeto es de un solo tipo (sin agregados ni una lógica compleja de inicialización) entonces el uso de un constructor simple es más que suficiente. Generalmente, sin embargo, las fábricas entregan un nivel de abstracción adicional muy útil. Una fábrica puede ser un método estático sencillo en la clase de entidad u otro componente independiente. La presencia de un método de fábrica también mejora la legibilidad, ya que expresa claramente por qué se está creando cada una de las instancias. Los constructores limitan la posibilidad de abordar diferentes situaciones de creación de instancias, ya que no tienen nombre y solo se pueden distinguir por su firma. Sobre todo en el caso de las firmas largas, posteriormente cuesta entender por qué se obtiene una instancia determinada. En la figura 3 se muestra el método de fábrica de la clase Customer.

Figura 3 Método de fábrica de la clase Customer

public static Customer CreateNewCustomer(
  String id, String companyName, String contactName)
{
  Contract.Requires<ArgumentNullException>(
           id != null, "id");
  Contract.Requires<ArgumentException>(
           !String.IsNullOrWhiteSpace(id), "id");
  Contract.Requires<ArgumentNullException>(
           companyName != null, "companyName");
  Contract.Requires<ArgumentException>(
           !String.IsNullOrWhiteSpace(companyName), "companyName");
  Contract.Requires<ArgumentNullException>(
           contactName != null, "contactName");               
  Contract.Requires<ArgumentException>(
           !String.IsNullOrWhiteSpace(contactName), "contactName");
 
  var c = new Customer
              {
                Id = id,
                Name = companyName,
                  Orders = new List<Order>(),
                ContactInfo = new ContactInfo
                              {
                                 ContactName = contactName
                              }
              };
  return c;
}

Los métodos de fábrica son atómicos. Reciben los parámetros de entrada, realizan su labor y devuelven una nueva instancia de un tipo determinado. Las instancias devueltas siempre debieran estar en un estado válido. La fábrica es la responsable de cumplir con todas las reglas de validación internas.

La fábrica también tiene que validar los datos de entrada. Para esto, el uso de las precondiciones de Code Contracts permite mantener un código limpio y fácilmente legible. También se pueden emplear las condiciones posteriores para garantizar que las instancias devueltas se encuentren en un estado válido. Por ejemplo:

Contract.Ensures(Contract.Result<Customer>().IsValid());

En cuanto al uso de invariantes dentro de la clase, la experiencia nos dice que no siempre podemos darnos el lujo de usarlas. Las invariantes pueden ser demasiado invasivas, sobre todo en los modelos grandes y complejos. Las invariantes que proporciona Code Contracts a veces son demasiado respetuosas del conjunto de reglas, y a veces necesitamos más flexibilidad en el código. Por lo tanto resulta preferible restringir los lugares donde se deben hacer cumplir las invariantes.

Validación

Es probable que haya que validar las propiedades en las clases de dominio para asegurar que ningún campo quede vacío, que los contenedores limitados no contengan ningún texto demasiado largo, que los valores caigan en los rangos adecuados, y así sucesivamente. También hay que plantearse la posibilidad de realizar una validación cruzada de las propiedades y reglas de negocio sofisticadas. ¿Cómo codificar la validación? 

Como la validación consiste en código condicional, en definitiva se trata de combinar algunas instrucciones if devolver un valor booleano. Probablemente se podría escribir un nivel de validación con código llano y sin ningún marco ni tecnología, pero esto no es una buena idea. El código resultante no sería muy legible, y no podría se podría desarrollar fácilmente. Pero algunas bibliotecas fluidas permiten facilitar esto. Enfrentada a las reglas de negocio de la vida real, la validación puede ser altamente volátil, y la implementación debe dar cuenta de esto. Al fin y al cabo, no basta con escribir código que valida; hay que escribir código que sea capaz de validar los mismos datos frente a diferentes reglas.

En la validación, a veces hay que pegar un grito cuando se pasan los datos y otras veces simplemente hay que recopilar los errores e informarlos a otros niveles del código. Recuerde que los contratos de Code Contracts no validan; comprueban las condiciones y luego generan una excepción si no se cumple la condición. Al usar un controlador de errores centralizado es posible recuperarse de las excepciones y adaptarse en forma limpia. Por lo general, en las entidades de dominio recomiendo usar Code Contracts solo para captar los errores potencialmente severos que pueden conducir a estados incoherentes. Sí tiene sentido usar Code Contracts en las fábricas; en este caso, si los datos que se pasan no son válidos, el código tiene que generar una excepción. La decisión de usar Code Contracts en los métodos establecedores de las propiedades está en sus manos. Yo prefiero tomar un camino menos exigente y validar por medio de los atributos. ¿Pero qué atributos?

Anotaciones de datos frente a VAB

El espacio de nombres de las Anotaciones de datos y Validation Application Block de Enterprise Library son muy parecidos. Los dos marcos están basados en atributos y se pueden extender con clases personalizadas para representar las reglas personalizadas. Ambos permiten definir una validación cruzada de las propiedades. Y por último, ambos marcos tienen una API de validación que evalúa una instancia y devuelve una lista con los errores. ¿Y cuáles son las diferencias?

Las Anotaciones de datos forman parte de Microsoft .NET Framework y no requieren de una descarga separada. Enterprise Library, en cambio, es una descarga aparte; esto no es un problema grave en un proyecto grande, paro puede se engorroso ya que puede requerir de una aprobación en el caso de algunas empresas. Enterprise Library se instala fácilmente mediante NuGet (consulte el artículo “Administre las bibliotecas de proyecto con NuGet” en este número).

Enterprise Library VAB es mejor que las Anotaciones de datos en un aspecto: Se puede configurar por medio de conjuntos de reglas en XML. Un conjunto de reglas en XML es una entrada del archivo de configuración en la que se describe la validación deseada. Huelga decir, que esto se puede cambiar en forma declarativa sin tocar si quiera el código. En la figura 4 se ofrece un conjunto de reglas de muestra.

Figura 4 Conjuntos de reglas de Enterprise Library

<validation>
   <type assemblyName="..." name="ValidModel1.Domain.Customer">
     <ruleset name="IsValidForRegistration">
       <properties>
         <property name="CompanyName">
           <validator negated="false"
                      messageTemplate="The company name cannot be null" 
                      type="NotNullValidator" />
           <validator lowerBound="6" lowerBoundType="Ignore"
                      upperBound="40" upperBoundType="Inclusive" 
                      negated="false"
                      messageTemplate="Company name cannot be longer ..."
                      type="StringLengthValidator" />
         </property>
         <property name="Id">
           <validator negated="false"
                      messageTemplate="The customer ID cannot be null"
                      type="NotNullValidator" />
         </property>
         <property name="PhoneNumber">
           <validator negated="false"
                      type="NotNullValidator" />
           <validator lowerBound="0" lowerBoundType="Ignore"
                      upperBound="24" upperBoundType="Inclusive"
                      negated="false"
                      type="StringLengthValidator" />
         </property>
         <property name="FaxNumber">
           <validator negated="false"
                      type="NotNullValidator" />
           <validator lowerBound="0" lowerBoundType="Ignore"
                      upperBound="24" upperBoundType="Inclusive"
                      negated="false"
                      type="StringLengthValidator" />
         </property>
       </properties>
     </ruleset>
   </type>
 </validation>

El conjunto de reglas enumera los atributos que queremos aplicar a una propiedad dada en un tipo dado. En código, el conjunto de reglas se valida del siguiente modo:

public virtual ValidationResults ValidateForRegistration()
{
  var validator = ValidationFactory
          .CreateValidator<Customer>("IsValidForRegistration");
  var results = validator.Validate(this);
  return results;
}

El método aplica los validadores enumerados en el conjunto de reglas IsValidForRegistration a la instancia especificada.

Una última observación sobre la validación y las bibliotecas. No abarqué todas las bibliotecas de validación populares, pero eso no cambiaría las cosas de manera significativa. El punto importante que hay que evaluar es si las reglas de negocio cambian y con qué frecuencia lo hacen. Basado en esto, se puede decidir si conviene más usar Anotaciones de datos, VAB, Code Contracts u otra biblioteca. En mi experiencia, si uno sabe exactamente lo que desea lograr, entonces resulta muy fácil elegir la biblioteca de validación “correcta”.

En resumen

Es muy difícil que el modelo de objetos para un dominio de negocio real sea una colección simple de propiedades y clases. Por lo demás, las cuestiones del diseño tienen prioridad sobre las tecnologías. Un modelo de objetos bien hecho expresa cada aspecto necesario del dominio. La mayoría de las veces, esto significa que habrá clases que serán fáciles de inicializar y validar, y contendrán abundantes propiedades y lógica. No hay que adherirse a las prácticas de DDD en forma dogmática; en lugar de esto, hay que entenderlas como guías que muestran el camino a seguir.     

Dino Espositoes autor de “Programming Microsoft ASP.NET 4” (Microsoft Press, 2011) y “Programming Microsoft ASP.NET MVC” (Microsoft Press, 2011), y coautor de “Microsoft .NET: Architecting Applications for the Enterprise” (Microsoft Press, 2008). Con residencia en Italia, Esposito participa habitualmente en conferencias y eventos del sector en todo el mundo. Puede seguir a Dino por Twitter en twitter.com/despos.

Gracias a los siguientes expertos técnicos por su ayuda en la revisión de este artículo: *Manuel Fahndrich y *Andrea Saltarello