问题
I'm fairly new to DDD world and after reading couple of books about it (Evans DDD among them) I was unable to find the answer to my question on internet: what the proper way of creating child entities with DDD? You see, a lot of information on internet operates on some simple level. But devils in the details and they're always omitted in dozens of DDD samples for the sake of simplicity.
I'm coming from my own answer on similair question here on stackoverflow. I'm not completely satisfied with my own vision on this problem so I thought I need to elaborate on this matter.
For example, I need to create simple model that represent cars naming: company, model and modification (for example, Nissan Teana 2012 - that will be "Nissan" company, "Teana" model and "2012" modification).
The sketch of the model I want to create looks like this:
CarsCompany
{
Name
(child entities) Models
}
CarsModel
{
(parent entity) Company
Name
(child entities) Modifications
}
CarsModification
{
(parent entity) Model
Name
}
So, now I need to create code. I'll use C# as language and NHibernate as ORM. This is important and what typically does not shown in vast DDD samples on the internet.
The first approach.
I'll start with simple approach with typical object creation via factory methods.
public class CarsCompany
{
public virtual string Name { get; protected set; }
public virtual IEnumerable<CarsModel> Models { get { return new ImmutableSet<CarsModel> (this._models); } }
private readonly ISet<CarsModel> _models = new HashedSet<CarsModel> ();
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<CarsModification> Modifications { get { return new ImmutableSet<CarsModification> (this._modifications); } }
private readonly ISet<CarsModification> _modifications = new HashedSet<CarsModification> ();
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
};
}
}
The bad thing about this approach is that creation of the model does not adding it to the parent models collection:
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 ();
}
After the transaction is committed and the session is flushed, the ORM will correctly write all into the database and next time we load that company it's models collection will correctly holds our model. The same goes to modification. So this approach leaves our parent entity in inconsistent state until it's been reload from database. No go.
The second approach.
This time we'll gonna use language specific option to solve the problem of setting protected properties of other classes - namely we'll gonna use "protected internal" modifier on both setters and constructor.
public class CarsCompany
{
public virtual string Name { get; protected set; }
public virtual IEnumerable<CarsModel> Models { get { return new ImmutableSet<CarsModel> (this._models); } }
private readonly ISet<CarsModel> _models = new HashedSet<CarsModel> ();
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<CarsModification> Modifications { get { return new ImmutableSet<CarsModification> (this._modifications); } }
private readonly ISet<CarsModification> _modifications = new HashedSet<CarsModification> ();
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 ();
}
This time each entity creation leaves both parent and child entity in consistent state. But validation of the child entity state leaked into the parent entity (AddModel
and AddModification
methods). Since I'm nowhere expert in DDD I'm not sure if it's ok or not. It could create more problems in future when child entities properties could not be simply set via properties and setting up some state based on passed parameters would require more complex work that assigning parameter value to property. I was under impression that we should concentrate logic about entity inside that entity wherever it's possible. For me this approach turns parent object into some kind of Entity&Factory hybrid.
The third approach.
Ok, we'll gonna invert the responsibilities of maintaining parent-child relations.
public class CarsCompany
{
public virtual string Name { get; protected set; }
public virtual IEnumerable<CarsModel> Models { get { return new ImmutableSet<CarsModel> (this._models); } }
private readonly ISet<CarsModel> _models = new HashedSet<CarsModel> ();
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<CarsModification> Modifications { get { return new ImmutableSet<CarsModification> (this._modifications); } }
private readonly ISet<CarsModification> _modifications = new HashedSet<CarsModification> ();
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 ();
}
This approach got all validation/creation logic inside corresponding entities and I don't know if it's good or bad, but by simple creation of the object with factory method we implicitly adding it to the parent object children collection. After transaction commit and session flush there will be 3 inserts into the database even tho I never written some "add" command in my code. I don't know maybe it's just me and my vast experience outside of DDD world but it feels a bit unnatural for now.
So, what's most correct way of adding child entities with DDD?
回答1:
I've got acceptable answer here: https://groups.yahoo.com/neo/groups/domaindrivendesign/conversations/messages/23187
Basically, it's a combination of method 2 and 3 - put AddModel method into the CarsCompany and make it call protected internal constructor of the CarsModel with name parameter which is validated inside the CarsModel's constructor.
回答2:
Here's a very concrete and burtally honest answer: all your approaches are wrong, because you broke the 'first rule' of DDD, that is, the DB does not exist.
What you're defining is PERSISTENCE model for an ORM (nhibernate). In orde to design the domain objects, first you have to identify the Bounded Context, its Model,the Entities and Value objects for that model and the Aggregate Root (which will deal internally with the children and busines rules).
Nhibernate or db schema have no place here, you need only pure C# code and a clear understanding of the domain.
回答3:
So, what's most correct way of adding child entities with DDD?
The third approach is called Tight Coupling. Company
, Car
and Modification
know almost everything about each other.
The second approach is widely proposed in DDD. An domain object is responsible for creating a nested domain object AND registering it inside.
The first approach is classic OOP style. Creation of an object is separated from adding an object into some collection. This way code consumer can substitute an object of a concrete class (e.g. Car) with an object of any derived class (e.g. TrailerCar).
// var model = CarsModel.Create (company, "Tiana");
var model = TrailerCarsModel.Create (
company, "Tiana", SimpleTrailer.Create(company));
company.AddModel (model);
Try adopting this business logic change in the 2nd / 3rd approach.
回答4:
Interesting. DDD vs Repository / ORM navigation properties. I think the answer depends on whether you are dealing with one aggregate or two. Should CarsModel be part of CarsCompany aggregate, or perhaps its own aggregate?
Approach one is to make the problem go away. MikeSW hinted at this. If CarsCompany and CarsModel don't need to be part of the same aggregate then they should only reference each other by identity, navigation properties should not be visible in the Domain.
Approach two is to treat adding to a relationship the same way we treat fetching an aggregate - make the Application Services call a method from the repository, which is correct place for your ORM-specific concerns to be addressed. Such a method could populate both ends of the relationship.
来源:https://stackoverflow.com/questions/11022036/proper-way-of-creating-child-entities-with-ddd