It’s common to find software engineers designing Entities or Aggregate Roots with public setters for all properties, often neglecting validation and business rules like the following code:
However, this violates the encapsulation principle in DDD, compromising consistency and hindering separation of concerns.
Here are some of the problems with this code from a DDD perspective:
- 1. Lack of Encapsulation: In DDD, entities should encapsulate their state and behavior. By using public setters for all properties, the code exposes the internal state of the Order class to external entities. This breaks encapsulation, making it difficult to enforce business rules and maintain consistency.
- 2. Public Exposed Navigation Property: The Customer property is defined as virtual and exposed as a public property. In DDD, it’s generally recommended to encapsulate relationships between aggregates and avoid exposing navigation properties directly. By exposing the Customer property, you tightly couple the Order and Customer aggregates, which can lead to various issues like data inconsistency and performance problems.
- Lack of Validation and Business Rules: The code lacks explicit validation and enforcement of business rules. With public setters, external entities can directly modify the properties without any validation, leading to invalid or inconsistent states. In DDD, it’s important to validate and enforce business rules within the domain entities.
- Mutable State: Allowing external entities to directly modify the properties of an Order instance introduces mutable state. Mutable state can make it challenging to reason about the system’s behavior, track changes, and maintain consistency. In DDD, it’s generally recommended to favor immutability or controlled mutability.
Lets refactor the code to enforce encapsulation,
The Order aggregate root uses public properties with private setters for all attributes, including CustomerId, OrderLineIds, ShippingAddress, BillingAddress, and OrderDate. It maintains encapsulation and prevents direct modification of the aggregate’s state.
The constructor takes the required parameters to create a valid order entity. Other methods can be added as necessary to handle behavior and enforce business rules. By using a constructor to initialize the aggregate’s properties, you ensure that the aggregate is created in a consistent and valid state.
Encapsulating the properties and exposing behavior through methods allows for better control over the aggregate’s state and promotes adherence to business rules.
Now, create a private ValidateOrder method within the Order aggregate to check if the order is valid based on certain conditions. This method can be reused from the constructor to perform the validation when creating a new instance of the Order aggregate.
Here’s the full example of Order aggregate root:
In this updated code, the Order class includes:
- Internal constructor: An internal constructor is used to set the properties of the Order aggregate during creation.
- Private method (ValidateOrder): The ValidateOrder method is a helper method used internally to validate the necessary properties of the order.
- CalculatePrice method: This method calculates the total price of the order based on the provided dictionary of product prices. It validates the order before performing the calculation.
- ChangeStatus method: This method allows you to change the status of the order while enforcing the defined business rules.
- ApplyDiscount method: This method applies a discount to the total price of the order, reducing the value by the provided discount amount. It also validates the order before applying the discount.
The OrderStatus enum remains the same.
The OrderManager class focuses on its responsibility of managing order-related operations and validating the necessary data. The calling code can then decide how to handle persistence and apply additional business logic before saving changes to the database:
- CreateOrder: Returns the newly created Order object.
- UpdateOrder: Returns the updated Order object.
- ApplyDiscount: Returns the updated total price (decimal) after applying the discount.
- ChangeStatus: Returns the updated OrderStatus after changing the status.
- CalculatePrice: Returns the calculated total price (decimal) of the order.
These modified return types allow the calling code to retrieve and utilize the relevant information or results from the OrderManager methods as needed.
In this article, we embarked on a journey through the realm of DDD Aggregate Roots, uncovering common pitfalls and providing step-by-step refactoring guidelines. By revisiting an initial flawed implementation of an Order entity, we highlighted the issues that arise when key DDD principles are disregarded.