One of the challenges of domain driven design is to keep the domain model simple. Entities form the core of a domain model. As the application becomes useful the domain model becomes complex. The entity classes become large in size, having a lot of fields and methods. More and more fields are added to the entity because thats where they belong. As more fields get added one has to add methods to the entity class following the tell-don't-ask principle. With decoupled domain model one can remove the responsibility of construction of other data representations from the entity hence reducing the number of methods. Also, the use of extract class refactoring allows one to create smaller classes. This creation of smaller classes requires deeper understanding of the domain and the entity under question. We would see in this chapter than with better understanding of the entity the class size (and cognitive weight imposed by it) can be reduced even before applying the extract class refactoring approach.
Let's dig a bit deeper in the entity. On the surface an entity class is made up of primitives, other entities and children entities. This level understanding of the structure of a entity doesn't provide useful answers, so lets categorize these field types. This classification uses functional and life-cycle behavior.
Taxonomy of an entity
Functional
Lets look at our Loan entity as an example.
class Loan {
//Infrastructure identification
long id;
//Infrastructure attributes
long version;
//Extrinsic infrastructure attributes
long createdByUserId;
long lastModifiedByUserId;
DateTime creationTime;
DateTime modificationTime;
//Business identification
String loanNumber;
//Business attributes
Money amount;
double interestRate;
double numberOfInstallments;
LocalDate startDate;
LoanFees fees;
InstallmentPaymentSchedule schedule;
//Extrinsic attributes
String purposeOfLoan;
String notes;
//Business associations
LoanProduct product;
Customer customer;
//Extrinsic associations
Fund fund;
LoanProduct product;
}
One of the core ideas of domain driven design is to isolate the domain model from other programming concerns like presentation, persistence, remote interaction etc. The loan entity definition would do that. The source of fund, purpose of loan, etc are not important to us because loan behavior is affected by them in our application. In another application this might not be the case, if for example, only certain percentage of a fund can be utilized for a certain kind of loan. In other words they are domain specific. The infrastructure fields might be useful to us from auditing purpose. The reason it is categorized as infrastructure is because they apply to most entities in the application. Typically such concerns are handled centrally and are orthogonal to domain concerns. So if we look closely it is the business attributes and associations that we really care about, when we are in the domain layer. Hence our loan class can be structured as:
class Loan extends ExtrinsicLoanData {
//Business attributes
Money amount;
double interestRate;
double numberOfInstallments;
LocalDate startDate;
LoanFees fees;
InstallmentPaymentSchedule schedule;
//Business associations
Customer customer;
}
class ExtrinsicLoan extends Entity {
String loanNumber;
Fund fund;
String purposeOfLoan;
String notes;
String loanNumber;
LoanProduct product;
}
class Entity {
long id;
long version;
long createdByUserId;
long lastModifiedByUserId;
DateTime creationTime;
DateTime modificationTime;
}
We have created a smaller Loan class in terms of its responsibilities. There is nothing more to sub-classing here than separation of responsibilities in separate files and one can use partial classes in C#. It is important to stress that such separation should not be done arbitrarily to make the classes look small. Even though deep class hierarchy is not desirable the fact that one call give it name which illustrates the nature of class is better than partial class. In the rest of the chapter we would learn more about how this insight helps in programming difficulties.
The extrinsic fields might not play a role in business logic in the application but might be useful for reporting purposes. Such reporting kind of queries where extrinsic fields are used to filter and join data, has business value but no business logic.
Requirement
An entity can also be analyzed on another axis. Entity by definition have a life cycle existing in multiple functional states during this. In each of these states certain fields in entity are mandatory and others are optional. The functional classification done above is orthogonal to this, e.g. a extrinsic field can be mandatory or optional. The fields can be mandatory or optional based on state of the entity. (In our application loan can be in any of these states, New, Approved, Active, Risky and Closed.)
Visibility
Typically most data on entity is visible via the service layer to the external world. There are some fields in entity which are used for internal calculations only and need not be exposed outside. We can categorize them as internal and external fields.
Along with the characteristics of the fields on an entity the state of entity is also useful for understanding an entity in isolation from rest of the domain. An entity can be in multiple states in its lifecycle. Our loan entity exists in: New, Approved, Disbursed, Risk and Closed states. An new entity can be only in certain states depending on the domain. The rest of the states are arrived at by making state transitions in rather than directly jumping onto them. For example in our application a loan cannot be in a Risk state directly.
Entity Construction
Entity construction is different from object construction. With entity construction we are interested in creating a representation of domain concept. Object construction is one step in this process. With that differentiation in mind, lets look at entity construction which happens in following context. (when not using decoupled domain model the entities can be constructed in presentation/application layers as well)
Entity activation doesn't require any design choice. In a simple case, the columns from database row are mapped to the entity object. Modern OR mapping tool can work with private constructor and fields to achieve this.
Before we dive into other scenarios lets look at what options we have. We have two mechanism constructor and methods. There are two extremes here which we can employ. A constructor which takes all the field or setters for every field. Both of these are also quite common and they also come with their own set of issues.
Constructor takes all
public Loan(Money amount, double interestRate, double numberOfInstallments, LoanFees fees, LocalDate disbursementDate, LoanProduct product, Customer customer, String loanNumber, Fund fund, String purposeOfLoan, String notes) {
if (amount
}
Loan loan = new Loan(amount, double interestRate, double numberOfInstallments, fees, null, LoanProduct product, Customer customer, null, fund, purposeOfLoan, notes)
This ensures the object cannot be constructed in an invalid state. But if suffers from following weaknesses.
Set-every-field
public Loan() {
}
public Loan amount(Money amount) {
this.amount = amount;
return this;
}
public Loan interestRate(double rate) {
this.interestRate = rate;
return this;
}
//so on....
Loan loan = new Loan().amount(amount).interestRate(rate)....;
The provides complete ease and control in constructing an entity, but has following issues.
Each of these two extremes have significant downsides. If constructing an entity is difficult it would discourage one from writing tests. When tests become difficult to write some might stop writing them. A test which is cluttered with non-relevant details is hard to understand and maintain as well. These problem get elevated for tests like Application Service Test, which involves constructing multiple entities to setup the context for the test. On the other extreme, an entity which is constructed by setting all the object's fields via setter method creates design issues. The presence of setters encourages them to be used in non-construction context. This (coupled with getters) is a slide towards anemic domain model.
Our insight into taxonomy of an entity can help break this deadlock for large entities, helping us find the right balance. Before we look at how, let see some implications of this insight.
Based on these outcomes we can pragmatically reduce the entity construction problem which benefits from domain understanding as well as good object design principles. Lets see some code at last to see what all this means in the different context we have outlined earlier.
public Loan(Money amount, double interestRate, double numberOfInstallments, LoanFees fees, LoanProduct loanProduct, Customer customer) {
}
Loan loan = new Loan(amount, interestRate, numberOfInstallments, fees, customer);
loan.purpose(purpose).fund(fund).notes(notes).product(product)...;
//state transitions
loan.approve(approvedBy);
loan.disburse(disbursementDate);
We have taken three decisions here.