Within the bounded contexts, effort is focused on building really expressive models; models that reveal the intention more than the implementation. When this is achieved, concepts in the domain surface naturally and the models are flexible and are simpler to refactor.
The DDD patterns are more of an application of patterns from GoF, Fowler and others specifically in the area of modeling subject domains.
The most common patterns are described below.
Dealing with Structure
Entities
Entities are classes where the instances are globally identifiable and keep the same identity for life. There can be change of state in other properties, but the identity never changes.
In this example, the Address can change many times but the identity of the Client never changes, no matter how many other properties change state.
Value Objects
Value objects are lightweight, immutable objects that have no identity. While their values are more important, they are not simple data transfer objects. Value objects are a good place to put complex calculations, offloading heavy computational logic from entities. They are much easier and safer to compose and by offloading heavy computational logic from the entities, they help entities focus on their role of life-cycle trackers.
In this example, when the address of the Client changes, then a new Address value object is instantiated and assigned to the Client.
Value Objects have simple life cycles and can greatly simplify your model. They also are great for introducing type safety at compile time for statically typed languages, and since the methods on value objects should be side effect free, they add a bit of functional programming flavor too.
Cardinality of Associations
The greater the cardinality of associations between classes, the more complex the structure. Aim for lower cardinality by adding qualifiers.
Bi-directional associations also add complexity. Critically ask questions of the model to determine if it is absolutely essential to be able to navigate in both directions between two objects.
In this example, if we rarely need to ask a Person object for all its projects, but we always ask a Project object for all people in the roles of the project, then we can make the associations one directional. Direction is about honoring object associations in the model in memory. If we need to find all Project objects for a Person object, we can use a query in a Repository (see below) to find all Projects for the Person.
Services
Sometimes it is impossible to allocate behavior to any single class, be it an entity or value object. These are cases of pure functionality that act on multiple classes without one single class taking responsibility for the behavior. In such cases, a stateless class, called a service class, is introduced to encapsulate this behavior.
Aggregates
As we add more to a model, the object graph can become quite large and complex. Large object graphs make technical implementations such as transaction boundaries, distribution and concurrency very difficult. Aggregates are consistency boundaries such that the classes inside the boundary are 'disconnected' from the rest of the object graph. Each aggregate has one entity which acts as the 'root' of the aggregate.
When creating aggregates, ensure that the aggregate is still treated as a unit that is meaningful in the domain. Also, test the correctness of the aggregate boundary by applying the 'delete' test. In the delete test, critically check which objects in the aggregate (and outside the aggregate) will also be deleted, if the root was deleted.
Follow these simple rules for aggregates.
- The root has global identity and the others have local identity
- The root checks that all invariants are satisfied
- Entities outside the aggregate only hold references to the root
- Deletes remove everything in the aggregate
- When an object changes, all invariants must be satisfied.
Remember that aggregates serve two purposes: domain simplification, and technical improvements. There can be inconsistencies between aggregates, but all aggregates are eventually consistent with each other.
Dealing with Life Cycles
Factories
Factories manage the beginning of the life cycle of some aggregates. This is an application of the GoF factory or builder patterns. Care must be taken that the rules of the aggregate are honored, especially invariants within the aggregate. Use factories pragmatically. Remember that factories are sometimes very useful, but not essential.
Repositories
While factories manage the start of the life cycle, repositories manage the middle and end of the life cycle. Repositories might delegate persistence responsibilities to object-relational mappers for retrieval of objects. Remember that repositories work with aggregates too. So the objects retrieved should honor the aggregate rules.
Dealing with Behavior
Specification Pattern
Use the specification pattern when there is a need to model rules, validation and selection criteria. The specification implementations test whether an object satisfies all the rules of the specification. Consider the following class:
class Project {
public boolean isOverdue() { ' }
public boolean isUnderbudget() { ' }
}
The specification for overdue and underbudget projects can be decoupled from the project and made the responsibility of the other classes.
public interface ProjectSpecification {
public boolean isSatisfiedBy(Project p);
}
public class ProjectIsOverdueSpecification implements
ProjectSpecification {
public boolean isSatisfiedBy(Project p) { ' }
}
This makes the client code more readable and flexible too.
If (projectIsOverdueSpecification.isSatisfiedBy(theCurrentProject) { ' }
Strategy Pattern
The strategy pattern, also known as the Policy Pattern is used to make algorithms interchangeable. In this pattern, the varying 'part' is factored out.
Consider the following example, which determines the success of a project, based on two calculations: (1) a project is successful if it finishes on time, or (2) a project is successful if it does not exceed its budget.
public class Project {
boolean is SuccessfulByTime();
boolean is SuccessfulByBudget();
}
By applying the strategy pattern we can encapsulate the specific calculations in policy implementation classes that contain the algorithm for the two different calculations.
interface ProjectSuccessPolicy {
Boolean isSuccessful(Project p);
}
class SuccessByTime implements ProjectSuccessPolicy { ' }
class SuccessByBudget implements ProjectSuccessPolicy { ' }
Refactoring the original Project class to use the policy, we encapsulate the criteria for success in the policy implementations and not the Project class itself.
class Project {
boolean isSuccessful(ProjectSuccessPolicy policy) {
return policy.isSuccessful(this);
}
}
Composite Pattern
This is a direct application of the GoF pattern within the domain being modeled. The important point to remember is that the client code should only deal with the abstract type representing the composite element. Consider the following class.
public class Project {
private List<Milestone> milestones;
private List<Task> tasks;
private List<Subproject> subprojects;
}
A Subproject is a project with Milestones and Tasks. A Milestone is a Task with a due date but no duration. Applying a composite pattern, we can introduce a new type Activity with different implementations.
interface Activity {
public Date due();
}
public class Subproject implements Activity {
private List<Activity> activities;
public Date due() { ' }
}
public class Milestone implements Activity {
public Date due() { ' }
}
public class Task implements Activity {
public Date due() { ... }
public int duration() { ' }
}
Now the model for the Project is much simpler.
public class Project {
private List<Activity> activities;
}
A UML representation of this model is shown below.
{{ parent.title || parent.header.title}}
{{ parent.tldr }}
{{ parent.linkDescription }}
{{ parent.urlSource.name }}