I believe you are correct in your assumptions A and B around persistence ignorance.
How you would best accomplish lazy loading of database objects is greatly dependent on your particular problem and implementation. However, I will attempt a generic answer to how to do lazy loading while still maintaining separation of concerns between persistence and domain logic classes.
I tend to implement persistence ignorance using the following classes:
- Domain classes - e.g. Customer
- Provider / repository classes - e.g. CustomerProvider
- Generic database querying classes - e.g. DatabaseQuery
The DatabaseQuery class would be responsible for using the database driver to query the database and assemble the resulting data into a generic result set such as a DataTable. The CustomerProvider would be responsible for using the DatabaseQuery class to execute SQL against the database and use the results of that SQL to assemble Customer instances. Customer would be "pure" domain object that contained data and logic related to customers.
As for whether the provider classes should be in the business tier or the data tier, I don't have a strong opinion. I can see a case for both. The important part is that you separate the responsibilities across classes.
So now let's discuss lazy loading. Let's say I wanted Customer to have a collection of Orders, but I don't want to pull Orders out of the database unless the consumer tries to access them. I would create a property on Customer called Orders. In the getter of that property, I would check to see if some private orders field was populated or if it was null. If it is null, load the orders from the database using an OrderProvider. Otherwise, just return the collection that was already loaded.
In my opinion, the need for Customer to contact OrderProvider does not violate PI. Customer still doesn't know how it gets orders. It just knows that it gets them from OrderProvider. There might be other reasons to decouple Customer from OrderProvider, but I don't think PI is a problem here.
This assumes that you are doing persistence ignorance manually. If you are a using an ORM framework such as Entity Framework or Hibernate, then those frameworks generally have features for supporting lazy loading automagically.
There is one thing I don't like about your design. And that is fact you are going to have 3 additional classes for each aggregate root (Factory, Mapper, Repository) with semi-duplicate code in form of reading and setting properties everywhere. This will be problematic as you add and remove properties, because forgetting to change a single method might cause an error. And the more complex the domain entity is, the more of this duplicate code is. I don't want to see CompanyFactory.CreateExisting
when company has 10 properties and 2 entities in aggregate.
Second thing I might complain about is the IdentityFactory
. In DDD, the identity should either be related to domain, in which case you set it to some calculated value. Or it is transparent, in which case you can have DB take care of it. Adding some kind of factory for identity is IMO overenginering.
I also don't like the mapping. While I agree it might not be possible to use EntityFramework directly, you could at least try. EF is getting better with POCO lately and you might be surprised how powerfull it is. But it still isn't NHibernate. If you really want to create separate models, try thinking about using some auto-mapping library.
It is nice you are demoing the design on one simple class, but try also demoing or at least imagining how the design will work with dozens of aggregates that have lots of properties and entities. Also, write a code that will use this design. You might realize that while the design might look good from the inside, it is cumbersome to use from the outside. Also, no one here is going to tell you if the design is appropriate for your team and your domain.
Best Answer
a) DDD doesn't specify that domain objects shouldn't create domain objects. A domain object can very well create another domain object. An aggregate is responsible for enforcing a consistency boundary around a cluster of entities and value objects. As such, it could very well have behaviors which create entities. For example, the stereotypical sales order aggregate can create instances of order line entities (or value objects, depending on implementation). The important thing is to ensure that domain and entities are created or deleted in well defined places. Take a look at this article which evaluates aggregate creation in a domain model.
b) The mapping between a real world entity and the corresponding abstraction in code is never isomorphic. Abstractions in code are bound by technical constraints that cannot be overlooked. Code isn't meant to be an isomorphism of reality, it is only meant exhibit functional characteristics which fulfill sets of requirements. The similarity between reality and code is an aspect that we strive for to streamline the development process. In other words, if a real world entity creates something, it doesn't immediately translate to the corresponding abstraction in code, though it may - there is no hard rule.