I'm struggling with a basic issue. The project is in C#, but the issue is general.
I'm following the always valid object principle. As one example I have a product entity that has a mandatory property "ProductCategory". The allowed product categories are user defined and therefore persisted (in a database).
For type safety and better reading I defined Value Objects for various properties to encapsulate the business rules for those types. So there is a ProductCategory class. When creating a ProductCategory instance a factory method checks for example the max length of the string that is passed to the factory before creating the instance. This ensures that each instance of ProdcutCategory is valid.
Simple parameter checks like length are easy and straight forward. My question is where to implement the validation check against the possible values that are persisted. A repository for the allowed values hides the persistence technology and has a method Exists or IsValid.
Option 1) calling the repository from the application layer before calling the factory.
Here the domain layer has no dependency on the repository/infrastructure layer as many advocate. But the factory can't ensure a valid object anymore. It depends on the application layer that the business rule is implemented. Also each command that needs a ProductCategory has to check the repository, which violates DRY.
Option 2) calling the repository from the value object factory
Since factories are part of the domain layer the layer must have access to the repository, which introduces extra coupling. The benefit is that the business validity of ProductCategory is ensured by the object itself and can't be circumvented.
Is there another solution to this dilemma or are there any specific criteria that encourage one or the other option? Would it be OK, if the factory is programmed against an interface of the repository that is saved in the domain layer and only implemented by the repository in the infrastructure layer? I have tried to understand the different approaches, but in this case I feel lost.
CodePudding user response:
If I understand correctly, your ProductCategory value object takes care of its own invariants, but whether a particular ProductCategory can be assigned to a particular Product is user defined and stored in the database.
You could use DomainEvents:
- You assign the Category to the Product in the Domain model.
- The Product adds a DomainEvent "ProductCategoryAssigned".
- Before committing your unit of work a DomainEvent handler performs the check with the databse that the particular Category can be assigned to the Product, and throws if not.
This will prevent the transaction completing with an invalid state.
You could use ValidatorAggregate:
If you really want your Product to be "always valid", even before the unit of work is committed, then you could go for:
- Create a ValidCategoriesAggregate and Repository that loads the valid Categories based on Product properties.
- Your Product's "SetCategory" method (or factory method) declares a ValidCategoriesAggregate as a required parameter, forcing your application layer to get one before calling the factory method.
- In your factory method, you can call "ValidCategoriesAggregate.HasCategory(category)" and throw before creating the Product if not present.
Try to avoid injecting repositories into your domain model.