There are three main objects that can control the life-cycle of a domain object:
- Aggregates: Hold several objects in a root object and can control their creation, manipulation, query and deletion;
- Factories: Create new objects with a set of data;
- Repositories: Recreate persisted objects from queried data, possibly delegating the reconstruction of the actual object instance to a factory.
Aggregates are entities that own other entities or association objects. But how do we define ownership?
Ownership can be, for example, when in a web-shop we have a product, lets say a t-shirt, who has variants, lets say combinations of colours and sizes. There is one product with several variants. It makes no sense for one of those variants to be attached to another product, nor to exit without a product. Also, if we delete a product, we must delete its variants.
Another example would be a customer nationality. The nationality is an association between a customer and a country. In this case, the customer doesn’t own the country entity, but it owns the relation object.It would not make sense to delete a customer and keep the connection between that customer (that does not exist any more) and the country. So if we delete a customer, we also need to delete its associations.
Rules when using aggregates:
- The root entity has global identity;
- The root entity is ultimately responsible for the objects it owns;
- The owned objects can only be reached through the aggregate root;
- The owned objects can be used by external objects, but these can not keep references to them;
- When deleting an aggregate, all owned objects must be deleted as well.
Factories are not a reflection of the model, but they are needed at some point, to maintain the domain objects clear and clean of logic irrelevant to the domain.
Factories should be used when the construction of an object or aggregate is compolex. For example when, after creating the raw object, we still need to set its internal properties in order to have it usable, or when we need to create an aggregate owned objects, or when we are not sure of exactly what concrete object class we need and have to put in place some logic to make that decision.
Using factories is about segregating logic and the SRP, by leaving object creation to a specialized class.
There are mainly 3 types of factories:
- Factory method: A common usage is to have a factory method inside an aggregate root, so it can build its owned objects. An example would be to have, in a service class, a method to build the query class it uses, or a method to create an empty instance of the model it manipulates;
- Abstract factory class: Used when we need to decide what concrete subtype of object the client needs. The factory class would encapsulate both the decision logic and creation logic, or outsource the creation logic to a builder class;
- Builder class: Used when there is complex logic needed to build an object or full aggregate. This is different from the factory method in that the manufactured object does not belong to the manufacturer, and its different from the abstract factory in that it does not have logic to decide what concrete class to build, it always builds the same class.
We still have, of course, the class constructors, which should be used when object creation is trivial.
We can get objects either by creation (new or reconstructed), or by trasversal of an object that holds a reference to the object we want to get.
Although it might be handy to have all objects connected so we can always get an object by traversal, this involves creating and maintaining many artificial associations between objects, associations that are not part of the model (hence artificial). This practise should be avoided.
From the principles discussed before we can establish, about querying for objects:
- We don’t need to concern ourselves with transient objects, typically value objects. It wouldn’t make sense to search for something without identity, thus irrelevant;
- We don’t need to query for objects that are more suited to be found by traversal of associations. For example, finding an object internal to an aggregate, should only be done through the root of the aggregate. Therefore mostly only the aggregate roots need to be searchable;
- Mostly we only need to have query capabilities for entities (aggregate roots or otherwise). Nevertheless, very occasionally we might need to query for value objects or enumerated values.
Therefore we should:
- Create a repository for each type of object that needs global querying access, namely the entities, aggregate roots or not, but not for the internal objects of an aggregate;
- In a repository, create a global known interface to encapsulate insertion or removal of data;
- Add in the repository the specific methods to retrieve fully instantiated objects or collection of objects.
There are several advantages to using repositories:
- Provide a simple way to retrieve persisted objects;
- Decouple application and domain logic from the persistence technology;
- Makes testing easier by giving a way to provide a predetermined object or collection of objects to the logic we want to test.
We must remember these advantages and keep in mind that that is why we want to use repositories. The actual technical implementation of repositories can differ a lot, depending on what framework or ORM the project is using, but the goal is always abstracting the querying from the application and domain logics. Thus we should not try to force a specific implementation of repositories, and instead try to find affinities between what the framework or ORM provides and what the DDD concepts require. Then, we can use those affinities in a DDD way.
When designing the DB we should keep the DB model close to the Domain model and, if necessary:
- Sacrifice of DB normalization in favour of performance, for example have non-atomic fields in order to have less DB queries;
- Sacrifice of DB normalization in favour of simpler object mapping to the domain model.