Compartir a través de


Puntos de datos

Programación para un diseño guiado por el dominio: sugerencias para los desarrolladores enfocados en datos

Julie Lerman

Descargar el ejemplo de código

Este año el libro revolucionario de Eric Evans sobre diseño de software, “Domain-Driven Design: Tackling Complexity in the Heart of Software” (Addison-Wesley Professional, 2003, amzn.to/ffL1k), celebra su décimo aniversario. Evans incorporó en este libro muchos años de experiencia en orientación de grandes empresas en el proceso de desarrollo de software. Luego reflexionó durante varios años más sobre cómo encapsular los patrones que conducían al éxito de estos proyectos: la interacción con el cliente, el análisis de los problemas operativos que se tratan de resolver, la creación de equipos y la arquitectura del software. El foco de estos patrones es el dominio de la empresa y en conjunto componen el Diseño guiado por el dominio (DDD). El DDD nos permite modelar el dominio en cuestión. Estos patrones resultan de esta abstracción de los conocimientos sobre el dominio. Hasta el día de hoy, al releer los prólogos de Fowler y de Evans encontramos un resumen excelente de la esencia del DDD.

En la columna actual y en las siguientes dos compartiré algunas pistas que le han aportado claridad a mi cerebro centrado en los datos y acostumbrado a Entity Framework, a medida que me esmero en mejorar mi código con algunos patrones técnicos del DDD.

¿Por qué me interesa el DDD?

El primer contacto con el DDD lo tuve en una pequeña entrevista por vídeo en InfoQ.com con Jimmy Nilsson, un arquitecto respetado en la comunidad de .NET (y en otros lugares), que habló sobre LINQ to SQL y Entity Framework (bit.ly/11DdZue). Al final de la entrevista, le pidieron a Nilsson que nombrara su libro técnico preferido. Su respuesta: “Mi libro favorito de informática es el libro de Eric Evans, ‘Diseño guiado por el domino’. Creo que es como poesía. No solo el contenido es increíble; también lo puedes leer muchas veces y se lee como poesía”. ¡Poesía! En ese entonces, yo estaba escribiendo mi primer libro técnico, “Programming Entity Framework” (O’Reilly Media, 2009) y esa descripción me llamó muchísimo la atención. Así que tomé el libro de Evans y leí un poco para ver cómo era. Evans es un escritor hermoso y fluido. Y esto, junto con su visión perspicaz y naturalista del desarrollo de software, hace que la lectura del libro sea un verdadero placer. Pero también me sorprendió lo que leí. No solo la escritura era maravillosa; me llamó muchísimo la atención lo que escribía. Hablaba sobre crear relaciones con los clientes y entender realmente sus empresas y sus problemas de negocio (con respecto al software en cuestión), no solo producir líneas de código. En mis 25 años de desarrollo de software, esto ha sido algo importantísimo para mí. Quería más.

Di vueltas alrededor del DDD durante algunos años más, hasta que comencé a aprender de verdad: conocí a Evans en un congreso y luego asistí a su taller de inmersión de cuatro días. Aunque estoy lejos de ser una experta en DDD, descubrí que podía emplear el patrón del Contexto acotado de inmediato mientras me esforzaba por desplazar mi propio proceso de creación de software hacia una estructura más organizada y administrable. Puede leer sobre este tema en mi columna de enero de 2013, “Reducción de los modelos de EF con contextos acotados de DDD” (msdn.microsoft.com/magazine/jj883952).

Desde entonces he explorado más. El DDD me intriga y me inspira, pero debido a mi punto de vista centrado en los datos me cuesta entender algunos de los patrones técnicos responsables de su éxito. Creo que probablemente muchos desarrolladores pasen por las mismas dificultades, así que compartiré algunas de las lecciones que he aprendido con la ayuda, el interés y la generosidad de Evans y varios profesionales y profesores de DDD más como Paul Rayner, Vaughn Vernon, Greg Young, Cesar de la Torre e Yves Reynhout.

Al modelar el dominio, olvídese de la persistencia

La modelación del dominio gira totalmente en torno a las tareas de la empresa. Al diseñar los tipos con sus propiedades y comportamientos, siempre estoy tentada de pensar en cómo funcionarán las relaciones en la base de datos y cómo tratará mi marco de asignación relacional de objetos (ORM) predilecto, Entity Framework, las propiedades, relaciones y jerarquías de herencia que estoy creando. A menos que usted vaya a crear el software para una empresa que se dedica al almacenamiento y la recuperación de datos (algo así como Dropbox), la persistencia de datos solo juega un papel complementario en la aplicación. Es como realizar una llamada a la API de un servicio del tiempo para mostrarle al usuario la temperatura actual. O enviar datos de la aplicación a un servicio externo, por ejemplo, para registrarse en Meetup.com. Por supuesto, los datos pueden ser más complicados, pero si sigue un enfoque de DDD para la limitación de los contextos, se concentra en los comportamientos y se orienta en el DDD para crear los tipos, entonces la persistencia probablemente puede ser mucho menos compleja que los sistemas que crea actualmente.

Y si se aprendió la asignación relacional de objetos y sabe, por ejemplo, cómo configurar las asignaciones con la base de datos con la API fluida de Entity Framework, entonces también debería ser capaz de poner en marcha la persistencia como corresponde. En el peor de los casos, es posible que deba realizar algunos ajustes en sus clases. En casos extremos, como con una base de datos heredada, incluso podría agregar un modelo de persistencia diseñado para la asignación a la base de datos y usar luego algo como AutoMapper para resolver las cosas entre el modelo de dominio y el modelo de persistencia.

Pero estos temores no tienen ninguna relación con el problema de negocio que debe solucionar el software, así que la persistencia no debería interferir con el diseño del dominio. Para mí esto es muy difícil, ya que cuando diseño mis entidades no puedo evitar pensar en cómo deducirá EF las asignaciones a la base de datos. Así que trato de bloquear ese ruido.

Establecedores privados y métodos públicos

Otra regla práctica es hacer que los establecedores de propiedades sean privados. En vez de permitir que el código que realiza las llamadas establezca propiedades a diestra y siniestra, deberíamos controlar la interacción con los objetos del DDD y sus datos mediante métodos que modifican las propiedades. Y no, no me refiero a métodos como SetFirstName y SetLastName. Por ejemplo, en vez de crear una instancia de un nuevo tipo Customer y luego establecer cada una de sus propiedades, es posible que tengamos que tomar en cuentas algunas reglas cuando creamos un cliente nuevo. Podemos incorporar estas reglas en el constructor de Customer, emplear un método con el patrón de fábrica o incluso tener un método Create en el tipo Customer. En la Figura 1 se muestra un tipo Customer que se definió según el patrón de DDD de una raíz agregada (es decir, el “elemento primario” de un gráfico de objetos, denominado también “entidad raíz” en el DDD). Las propiedades de Customer tienen establecedores privados para que solo los otros miembros de la clase Customer puedan afectar estas propiedades directamente. La clase expone un constructor para controlar cómo se crea la instancia y oculta el constructor sin parámetros (requerido por Entity Framework) como interno.

Figura 1 Propiedades y métodos de un tipo que actúa como raíz agregada

public class Customer : Contact
{
  public Customer(string firstName,string lastName, string email)
  { ... }
  internal Customer(){ ... }
  public void CopyBillingAddressToShippingAddress(){ ... }    
  public void CreateNewShippingAddress(
   string street, string city, string zip) { ... }
  public void CreateBillingInformation(
   string street, string city, string zip,
   string creditcardNumber, string bankName){ ... }    
  public void SetCustomerContactDetails(
   string email, string phone, string companyName){ ... }
  public string SalesPersonId { get; private set; }
  public CustomerStatus Status{get;private set;}
  public Address ShippingAddress { get; private set; }
  public Address BillingAddress { get;private set; }
  public CustomerCreditCard CreditCard { get; private set; }
}

El tipo Customer controla y protege las otras entidades del agregado (algunas direcciones y el tipo de tarjeta de crédito), para lo cual expone determinados métodos específicos (como, por ejemplo, CopyBillingAddressToShippingAddress) con los que se crearán y manipularán estos objetos. La raíz agregada debe asegurarse de que las reglas que definen cada entidad dentro del agregado se apliquen según la lógica y el comportamiento del dominio que se implementó en esos métodos. Y lo más importante: la raíz agregada está a cargo de la lógica invariable y de la coherencia en todo el agregado. Hablaré más sobre las invariables en mi siguiente columna, pero mientras tanto, le recomiendo que lea el artículo de Jimmy Bogard “Strengthening Your Domain: Aggregate Construction” en su blog ( bit.ly/ewNZ52), que ofrece una excelente explicación de las invariables en los agregados.

Al final, lo que expone Customer es el comportamiento en vez de las propiedades: CopyBillingAddressToShippingAddress, CreateNewShipping­Address, CreateBillingInformation y SetCustomerContactDetails.

Observe que el tipo Contact, del que se deriva Customer, reside en un ensamblado diferente, llamado “Common”, ya que podríamos necesitarlo en otras clases. Tengo que ocultar las propiedades de Contact, pero no pueden ser privadas, ya que Customer tiene que poder tener acceso a ellas. En vez de esto, el ámbito se establece como Protected:

public class Contact: Identity
{
  public string CompanyName { get; protected set; }
  public string EmailAddress { get; protected set; }
  public string Phone { get; protected set; }
}

Una nota al margen sobre Identities: Customer y Contact podrían parecer objetos de valor del DDD, debido a que no tienen ningún valor clave. En mi solución, sin embargo, el valor clave lo proporciona la clase Identity, de la cual se deriva Contact. Y ninguno de estos tipos es inmutable, así que de todos modos no podrían ser objetos de valor.

Como Customer se deriva de Contact, tendrá acceso a esas propiedades protegidas y podrá establecerlas, como en el método SetCustomerContactDetails:

public void SetCustomerContactDetails (string email, string phone, string companyName)

{

  EmailAddress = email;

  Phone = phone;

  CompanyName = companyName;

}

A veces, CRUD es todo lo que necesitamos

No hace falta que usemos el DDD para crear todo lo que necesita la aplicación. La función del DDD es ayudarnos con los comportamientos complejos. Si solo necesitamos algunas ediciones o consultas al azar sin procesar, entonces una clase simple (o un conjunto de clases), definida como lo haríamos siempre con EF Code First (mediante propiedades y relaciones) y en combinación con métodos de inserción, actualización y eliminación (a través de un repositorio o simplemente con DbContext) es todo lo que necesitamos. Es decir, para algo así como la creación de un pedido con todos los elementos de línea, podríamos usar el DDD para guiarnos con las reglas y los comportamientos de negocio especiales. Por ejemplo, ¿esta persona que hace el pedido es un cliente Platinum? En este caso, tenemos que recuperar cierta información sobre el cliente para determinar si la respuesta es sí y, en caso afirmativo, aplicar un descuento del diez por ciento a todos los elementos que se agregan al pedido. ¿El usuario ya proporcionó la información de la tarjeta de crédito? Entonces es posible que tengamos que llamar a un servicio de comprobación para asegurarnos de que se trate de una tarjeta válida.

La clave en el DDD es incluir la lógica del dominio en forma de métodos dentro de las clases de entidad del dominio y aprovechar la programación orientada a objetos en vez de implementar “scripts transaccionales” dentro de estos objetos de negocio sin estado, que es la forma de una clase típica de Code First en un demoware. 

Pero, a veces, lo que hacemos es algo muy corriente, como crear el registro de un contacto (nombre, dirección, remitido por, etcétera) y guardarlo. Eso no es más que creación, lectura, actualización y eliminación (CRUD). No hace falta que creemos agregados, raíces y comportamientos solamente para esto.

Lo más probable es que la aplicación contenga una mezcla de comportamientos complejos y CRUD. Tómese el tiempo para aclarar los comportamientos y no pierda tiempo, energías ni dinero en una arquitectura excesiva para aquellas piezas de la aplicación que en realidad son muy simples. En estos casos, resulta importante identificar los límites entre los diferentes subsistemas o contextos acotados. Un contexto acotado podría guiarse básicamente por los datos (simplemente CRUD), mientras que un contexto crítico acotado por el dominio central debería, por otro lado, diseñarse según los principios de DDD.

Los datos compartidos pueden ser una maldición en los sistemas complejos

Otro quebradero de cabeza para mí, contra el cual despotriqué y del que me quejé cuando los demás amablemente trataban de explicarlo, tiene que ver con el uso compartido de tipos y datos entre los diferentes subsistemas. Resultó claro que no podría tener “el oro y el moro”, así que me vi obligada a reconsiderar mi creencia de que todos los tipos se debían compartir por fuerza entre los sistemas y que todos esos tipos debían interactuar con la misma tabla en la misma base de datos.

Estoy aprendiendo a evaluar dónde tengo que compartir los datos y a escoger luego con cuidado dónde invierto mis energías. Hay cosas que no vale la pena siquiera intentarlas, como la asignación desde diferentes contextos a una misma tabla o incluso una misma base de datos. El ejemplo más común es compartir un tipo Contact que trata de satisfacer las necesidades de todos los sistemas. ¿Cómo conciliar y aprovechar el control de código fuente para un tipo Contact que se podría emplear en varios sistemas? ¿Qué pasa si un sistema tiene que modificar la definición de este tipo Contact? Con respecto a la ORM, ¿cómo asignamos un Contact que se emplea en varios sistemas a una sola tabla en una sola base de datos?

El DDD se encarga de alejarnos de los modelos de dominio y datos compartidos, al explicarnos que no siempre tenemos que apuntar a la misma tabla de personas en una misma base de datos.

En mi caso, la mayor resistencia se debe a los 25 años que me concentré en a las ventajas de la reutilización: reutilización de código y reutilización de datos. Así que la siguiente idea me costó bastante esfuerzo, pero estoy empezando a tomarle el gustillo: no es ningún delito duplicar datos. Por supuesto que no todos los datos se ajustan a este paradigma nuevo (para mí). ¿Pero qué pasa, por ejemplo, con algo ligero como el nombre de una persona? ¿Qué pasa si duplicamos el nombre y apellido de una persona en varias tablas o incluso en varias bases de datos dedicadas a diferentes subsistemas de la solución de software? A la larga, si soltamos esa fijación con los datos compartidos, se simplifica enormemente la labor necesaria para construir el sistema. En todo caso, siempre debemos minimizar la duplicación de datos y atributos en contextos acotados diferentes. A veces solamente necesitamos el identificador de un cliente y su estado para calcular el descuento en un contexto acotado a los precios. Dentro de un contexto acotado a la administración, podría bastar con el nombre y apellido de ese mismo cliente.

Pero sigue habiendo tanta información que debemos compartir entre los subsistemas. Podemos aprovechar lo que el DDD llama una “capa contra daños” (que puede ser algo tan sencillo como un servicio o una cola de mensajes) para asegurarnos de que si alguien crea un contacto nuevo en un sistema, por ejemplo, entonces o reconocemos que esa persona ya existe en otro lugar o garantizamos que la persona se crea en otro subsistema, junto con una clave de identidad compartida.

Bastante para digerir hasta el próximo mes

A medida que avanzo en el aprendizaje y la comprensión del lado técnico del Diseño guiado por el dominio, luchando por reconciliar los antiguos hábitos con las ideas nuevas y pasando por incontables momentos de sorpresa, las pistas que presenté aquí son las que realmente me permitieron ver más luz que oscuridad. A veces no es más que un asunto de perspectiva y la forma en que la expresé aquí refleja la perspectiva que sirvió para aclarar las cosas para mí.

Compartiré otros de mis momentos de sorpresa en la siguiente columna, donde hablaré sobre ese término condescendiente que probablemente ya habrá oído mencionar: “modelo de dominio anémico”, junto con el primo en el DDD, el “modelo de dominio enriquecido”. También analizaré las relaciones unidireccionales y veremos qué podemos esperar cuando nos toca añadir una persistencia de datos al usar Entity Framework. También me referiré a algunos temas del DDD que me causaron mucha aflicción, para facilitarle la curva de aprendizaje.

Hasta entonces, ¿por qué no echa una mirada más detenida a sus propias clases y, dando rienda suelta al pequeño maniático del control que lleva dentro, oculta esos establecedores de propiedad y expone métodos más descriptivos y explícitos? Y recuerde: no se permiten los métodos del tipo “SetLastName”. ¡Eso sería trampa!

Julie Lerman ha recibido el premio al Profesional más valioso (MVP) de Microsoft, es consultora y mentor de .NET, y vive en las colinas de Vermont. Realiza presentaciones sobre acceso a datos y otros temas de Microsoft .NET en grupos de usuarios y congresos en todo el mundo. Mantiene un blog en thedatafarm.com/blog y es la autora de “Programming Entity Framework” (2010), además de una edición para Code First (2011) y una para DbContext (2012), todos de O’Reilly Media. Sígala en Twitter en twitter.com/julielerman y vea sus cursos de Pluralsight en juliel.me/PS-Videos

Gracias al siguiente experto técnico por su ayuda en la revisión de este artículo: Cesar de la Torre (Microsoft)