Forma adecuada de crear entidades hijas con DDD

Soy bastante nuevo en el mundo de DDD y después de leer un par de libros al respecto (Evans DDD entre ellos) no pude encontrar la respuesta a mi pregunta en Internet: ¿cuál es la forma correcta de crear entidades infantiles con DDD? Usted ve, una gran cantidad de información en Internet opera en algún nivel simple. Pero los demonios en los detalles y siempre se omiten en docenas de muestras de DDD en aras de la simplicidad.

Vengo de mi propia respuesta en la pregunta de similair aquí en stackoverflow. No estoy completamente satisfecho con mi propia visión sobre este problema, así que pensé que necesitaba desarrollar más sobre este asunto.

Por ejemplo, necesito crear un modelo simple que represente los nombres de los automóviles: compañía, modelo y modificación (por ejemplo, Nissan Teana 2012, que será la compañía “Nissan”, el modelo “Teana” y la modificación “2012”).

El boceto del modelo que quiero crear se ve así:

CarsCompany { Name (child entities) Models } CarsModel { (parent entity) Company Name (child entities) Modifications } CarsModification { (parent entity) Model Name } 

Entonces, ahora necesito crear código. Usaré C # como lenguaje y NHibernate como ORM. Esto es importante y lo que normalmente no se muestra en vastas muestras de DDD en Internet.

El primer acercamiento.

Comenzaré con un enfoque simple con la creación típica de objetos a través de métodos de fábrica.

 public class CarsCompany { public virtual string Name { get; protected set; } public virtual IEnumerable Models { get { return new ImmutableSet (this._models); } } private readonly ISet _models = new HashedSet (); protected CarsCompany () { } public static CarsCompany Create (string name) { if (string.IsNullOrEmpty (name)) throw new ArgumentException ("Invalid name specified."); return new CarsCompany { Name = name }; } public void AddModel (CarsModel model) { if (model == null) throw new ArgumentException ("Model is not specified."); this._models.Add (model); } } public class CarsModel { public virtual CarsCompany Company { get; protected set; } public virtual string Name { get; protected set; } public virtual IEnumerable Modifications { get { return new ImmutableSet (this._modifications); } } private readonly ISet _modifications = new HashedSet (); protected CarsModel () { } public static CarsModel Create (CarsCompany company, string name) { if (company == null) throw new ArgumentException ("Company is not specified."); if (string.IsNullOrEmpty (name)) throw new ArgumentException ("Invalid name specified."); return new CarsModel { Company = company, Name = name }; } public void AddModification (CarsModification modification) { if (modification == null) throw new ArgumentException ("Modification is not specified."); this._modifications.Add (modification); } } public class CarsModification { public virtual CarsModel Model { get; protected set; } public virtual string Name { get; protected set; } protected CarsModification () { } public static CarsModification Create (CarsModel model, string name) { if (model == null) throw new ArgumentException ("Model is not specified."); if (string.IsNullOrEmpty (name)) throw new ArgumentException ("Invalid name specified."); return new CarsModification { Model = model, Name = name }; } } 

Lo malo de este enfoque es que la creación del modelo no lo agrega a la colección de modelos padre:

 using (var tx = session.BeginTransaction ()) { var company = CarsCompany.Create ("Nissan"); var model = CarsModel.Create (company, "Tiana"); company.AddModel (model); // (model.Company == company) is true // but (company.Models.Contains (model)) is false var modification = CarsModification.Create (model, "2012"); model.AddModification (modification); // (modification.Model == model) is true // but (model.Modifications.Contains (modification)) is false session.Persist (company); tx.Commit (); } 

Una vez que se confirma la transacción y se vacía la sesión, el ORM escribirá todo correctamente en la base de datos y la próxima vez que carguemos esa compañía, su colección de modelos mantendrá correctamente nuestro modelo. Lo mismo pasa con la modificación. Por lo tanto, este enfoque deja a nuestra entidad matriz en un estado inconsistente hasta que se recarga de la base de datos. No vayas.

El segundo enfoque.

Esta vez usaremos la opción específica del idioma para resolver el problema de configurar propiedades protegidas de otras clases, es decir, usaremos el modificador “protegido interno” tanto en los instaladores como en el constructor.

 public class CarsCompany { public virtual string Name { get; protected set; } public virtual IEnumerable Models { get { return new ImmutableSet (this._models); } } private readonly ISet _models = new HashedSet (); protected CarsCompany () { } public static CarsCompany Create (string name) { if (string.IsNullOrEmpty (name)) throw new ArgumentException ("Invalid name specified."); return new CarsCompany { Name = name }; } public CarsModel AddModel (string name) { if (string.IsNullOrEmpty (name)) throw new ArgumentException ("Invalid name specified."); var model = new CarsModel { Company = this, Name = name }; this._models.Add (model); return model; } } public class CarsModel { public virtual CarsCompany Company { get; protected internal set; } public virtual string Name { get; protected internal set; } public virtual IEnumerable Modifications { get { return new ImmutableSet (this._modifications); } } private readonly ISet _modifications = new HashedSet (); protected internal CarsModel () { } public CarsModification AddModification (string name) { if (string.IsNullOrEmpty (name)) throw new ArgumentException ("Invalid name specified."); var modification = new CarsModification { Model = this, Name = name }; this._modifications.Add (modification); return modification; } } public class CarsModification { public virtual CarsModel Model { get; protected internal set; } public virtual string Name { get; protected internal set; } protected internal CarsModification () { } } ... using (var tx = session.BeginTransaction ()) { var company = CarsCompany.Create ("Nissan"); var model = company.AddModel ("Tiana"); var modification = model.AddModification ("2011"); session.Persist (company); tx.Commit (); } 

Esta vez, cada creación de entidad deja a la entidad principal y secundaria en un estado coherente. Pero la validación del estado de la entidad secundaria se filtró en la entidad principal (métodos AddModel y AddModification ). Como no soy experto en DDD, no estoy seguro de si está bien o no. Podría crear más problemas en el futuro cuando las propiedades de las entidades secundarias no pudieran establecerse simplemente a través de las propiedades y configurar un estado basado en parámetros pasados ​​requeriría un trabajo más complejo que asignar un valor de parámetro a una propiedad. Tenía la impresión de que deberíamos concentrar la lógica sobre la entidad dentro de esa entidad siempre que sea posible. Para mí, este enfoque convierte el objeto principal en algún tipo de híbrido de Entidad y Fábrica.

El tercer enfoque.

Ok, vamos a invertir las responsabilidades de mantener las relaciones entre padres e hijos.

 public class CarsCompany { public virtual string Name { get; protected set; } public virtual IEnumerable Models { get { return new ImmutableSet (this._models); } } private readonly ISet _models = new HashedSet (); protected CarsCompany () { } public static CarsCompany Create (string name) { if (string.IsNullOrEmpty (name)) throw new ArgumentException ("Invalid name specified."); return new CarsCompany { Name = name }; } protected internal void AddModel (CarsModel model) { this._models.Add (model); } } public class CarsModel { public virtual CarsCompany Company { get; protected set; } public virtual string Name { get; protected set; } public virtual IEnumerable Modifications { get { return new ImmutableSet (this._modifications); } } private readonly ISet _modifications = new HashedSet (); protected CarsModel () { } public static CarsModel Create (CarsCompany company, string name) { if (company == null) throw new ArgumentException ("Company is not specified."); if (string.IsNullOrEmpty (name)) throw new ArgumentException ("Invalid name specified."); var model = new CarsModel { Company = company, Name = name }; model.Company.AddModel (model); return model; } protected internal void AddModification (CarsModification modification) { this._modifications.Add (modification); } } public class CarsModification { public virtual CarsModel Model { get; protected set; } public virtual string Name { get; protected set; } protected CarsModification () { } public static CarsModification Create (CarsModel model, string name) { if (model == null) throw new ArgumentException ("Model is not specified."); if (string.IsNullOrEmpty (name)) throw new ArgumentException ("Invalid name specified."); var modification = new CarsModification { Model = model, Name = name }; modification.Model.AddModification (modification); return modification; } } ... using (var tx = session.BeginTransaction ()) { var company = CarsCompany.Create ("Nissan"); var model = CarsModel.Create (company, "Tiana"); var modification = CarsModification.Create (model, "2011"); session.Persist (company); tx.Commit (); } 

Este enfoque tiene toda la lógica de validación / creación dentro de las entidades correspondientes y no sé si es bueno o malo, pero mediante la simple creación del objeto con el método de fábrica, lo agregamos implícitamente a la colección de elementos secundarios del objeto principal. Después de la confirmación de la transacción y la descarga de la sesión, habrá 3 inserciones en la base de datos, aunque nunca escribí un comando “agregar” en mi código. No sé, quizás sea solo yo y mi vasta experiencia fuera del mundo DDD, pero por el momento se siente un poco antinatural.

Entonces, ¿cuál es la forma más correcta de agregar entidades secundarias con DDD?

Tengo una respuesta aceptable aquí: https://groups.yahoo.com/neo/groups/domaindrivendesign/conversations/messages/23187

Básicamente, es una combinación de los métodos 2 y 3: coloque el método AddModel en CarsCompany y conviértalo en un constructor interno protegido del CarsModel con un parámetro de nombre que se valida dentro del constructor CarsModel.

Entonces, ¿cuál es la forma más correcta de agregar entidades secundarias con DDD?

El tercer enfoque se llama acoplamiento apretado . Company , Car y Modification saben casi todo sobre el otro.

El segundo enfoque es ampliamente propuesto en DDD. Un objeto de dominio es responsable de crear un objeto de dominio nested Y registrarlo dentro.

El primer enfoque es el estilo clásico OOP. La creación de un objeto se separa de agregar un objeto a alguna colección. De esta manera, el consumidor de código puede sustituir un objeto de una clase concreta (por ejemplo, un automóvil) con un objeto de cualquier clase derivada (por ejemplo, TrailerCar).

 // var model = CarsModel.Create (company, "Tiana"); var model = TrailerCarsModel.Create ( company, "Tiana", SimpleTrailer.Create(company)); company.AddModel (model); 

Intente adoptar este cambio de lógica empresarial en el enfoque 2º / 3º.

Interesante. DDD vs Repositorio / ORM propiedades de navegación. Creo que la respuesta depende de si se trata de uno o dos agregados. ¿Debería CarsModel ser parte del agregado de CarsCompany, o tal vez su propio agregado?

El enfoque uno es hacer que el problema desaparezca. MikeSW insinuó esto. Si CarsCompany y CarsModel no necesitan ser parte del mismo agregado, entonces solo deben hacer referencia entre sí por identidad, las propiedades de navegación no deberían estar visibles en el Dominio.

El segundo enfoque es tratar la adición a una relación de la misma manera que tratamos la obtención de un agregado: haga que los Servicios de aplicación llamen un método desde el repository, que es el lugar correcto para abordar sus inquietudes específicas de ORM. Tal método podría poblar ambos extremos de la relación.

Aquí hay una respuesta muy concreta y burtalmente honesta: todos sus enfoques están equivocados, porque rompió la ‘primera regla’ de DDD, es decir, el DB no existe.

Lo que estás definiendo es un modelo de PERSISTENCIA para un ORM (nhibernate). Para diseñar los objetos de dominio, primero debe identificar el Contexto delimitado , su Modelo, las Entidades y los objetos de Valor para ese modelo y la Raíz de agregados (que tratará internamente con las reglas de los niños y las empresas).

El esquema de Nhibernate o db no tiene lugar aquí, solo necesita código C # puro y una comprensión clara del dominio.