C # / DDD: how to model objects with internal state objects not implemented at the domain level when using onion architecture?

I am in the process of migrating a β€œbig ball of mud” (BBOM) system towards a system based on domain-driven design ideas.

After various iterations of refactoring, domain aggregates / entities are currently modeled using internal state objects, as described by Vonn Vernon in this article, for example: https://vaughnvernon.co/?p=879#comment-1896

Thus, an entity might look like this:

public class Customer { private readonly CustomerState state; public Customer(CustomerState state) { this.state = state; } public Customer() { this.state = new CustomerState(); } public string CustomerName => this.state.CustomerName; [...] } 

Today, the state object in this system is always a shell of the database table, based on the currently used proprietary data access structure of the application, which resembles the Active Record template. Thus, all state objects are inherited from the basic part of the data access structure. It is currently not possible to use POCOs as a state object, Entity Framework, or any of them.

Currently, the application uses the classic layer architecture in which the infrastructure (including the specified wrappers / state objects) is at the bottom, and then the domain. The domain knows that the infrastructure and repositories are implemented in the domain using the infrastructure. As you can see above, most objects contain a public constructor for conveniently creating new instances within the domain, which internally simply creates a new state object (because the domain knows it).

Now we would like to once again develop this and gradually turn the architecture around, resulting in a bow architecture. In this architecture, the domain will contain only the repository interfaces, and actual implementations will be provided by the infrastructure layer located on top of it. In this case, the domain can no longer know the actual wrappers of the state / database object table.

One idea for solving this problem would be for state objects to implement the interfaces defined by the domain, and at the moment this really looks like a good solution. It is also technically possible because, although state objects must be inherited from a special base data access class, they are free to implement interfaces. So the above example would change to something like:

 public class Customer { private readonly ICustomerState state; public Customer(ICustomerState state) { this.state = state; } public Customer() { this.state= <<<-- what to do here??; } [...] } 

So, when the repository (now implemented in the infrastructure) creates an instance of the new Client, it can easily go through the database shell object that implements ICustomerState. So far so good

However, when creating new objects in a domain, it is no longer possible to create an object of internal state, since we no longer know its actual implementation.

There are several possible solutions for this, but none of them seem really attractive:

  • We can always use abstract factories to create new objects, and these plants will then be implemented by infrastructure. Although there are certain cases where the factory domain is suitable due to the complexity of the object, I would not want to use it in each case, since they lead to a lot of interference in the domain and to another dependency that has passed around.
  • Instead of directly using the shells of the database table as state objects, we could use another class (POCO), which simply stores the values ​​and then is transferred from / to the database wrappers by the infrastructure. This may work, but it will lead to a lot of additional matching code and lead to 3 or more classes of the database table (DB shell, state object, domain object), which complicates maintenance. We would like to avoid this if possible.
  • To avoid going around factories, the constructor inside the object can call some magic single-element method StateFactory.Instance.Create<TState>() to create an internal state object. Then the responsibility for the infrastructure will be to register the appropriate implementation. Similarly, one could somehow get the DI container and allow the factory from there. I personally don't like this Service Locator approach, but it might be acceptable in this special case.

Are there any better options that I am missing?

+5
source share
1 answer

A domain-driven project is not a god suitable for a large ball of mud. Trying to use DDD on large systems is not as effective as object-oriented design. Try to think in terms of an object that collaborates together and hides the complexity of the data and begins to think about methods / behavior to manipulate internal objects through behavior.
To achieve onion architecture, I would suggest the following rules:

  • Try to avoid Orms (EF, Hibernate, etc.) in your business rules, as it adds database complexity (DataContext, DataSet, getters, setters, anemic models, code smells, etc.) to business code.
  • Composition is used in business rules, the key is to enter objects (actors in the system) through the designers, try to have purity in business rules.
  • Ask the object to do something with the data.
  • Time to invest in the design of the API object.
  • Leave the completion information to the end (database, cloud, mongo, etc.). You must implement the details in the class and not allow the complexity of distributing code outside it.
  • Try not to install design templates in your code always, only if necessary.

Here's how I could design business rules with objects for readability and maintenance:

 public interface IProductBacklog { KeyValuePair<bool, int> TryAddProductBacklogItem(string description); bool ExistProductBacklogItem(string description); bool ExistProductBacklogItem(int backlogItemId); bool TryDeleteProductBacklogItem(int backlogItemId); } public sealed class AddProductBacklogItemBusinessRule { private readonly IProductBacklog productBacklog; public AddProductBacklogItemBusinessRule(IProductBacklog productBacklog) { this.productBacklog = productBacklog ?? throw new ArgumentNullException(nameof(productBacklog)); } public int Execute(string productBacklogItemDescription) { if (productBacklog.ExistProductBacklogItem(productBacklogItemDescription)) throw new InvalidOperationException("Duplicate"); KeyValuePair<bool, int> result = productBacklog.TryAddProductBacklogItem(productBacklogItemDescription); if (!result.Key) throw new InvalidOperationException("Error adding productBacklogItem"); return result.Value; } } public sealed class DeleteProductBacklogItemBusinessRule { private readonly IProductBacklog productBacklog; public DeleteProductBacklogItemBusinessRule(IProductBacklog productBacklog) { this.productBacklog = productBacklog ?? throw new ArgumentNullException(nameof(productBacklog)); } public void Execute(int productBacklogItemId) { if (productBacklog.ExistProductBacklogItem(productBacklogItemId)) throw new InvalidOperationException("Not exists"); if(!productBacklog.TryDeleteProductBacklogItem(productBacklogItemId)) throw new InvalidOperationException("Error deleting productBacklogItem"); } } public sealed class SqlProductBacklog : IProductBacklog { //High performance, not loading unnesesary data public bool ExistProductBacklogItem(string description) { //Sql implementation throw new NotImplementedException(); } public bool ExistProductBacklogItem(int backlogItemId) { //Sql implementation throw new NotImplementedException(); } public KeyValuePair<bool, int> TryAddProductBacklogItem(string description) { //Sql implementation throw new NotImplementedException(); } public bool TryDeleteProductBacklogItem(int backlogItemId) { //Sql implementation throw new NotImplementedException(); } } public sealed class EntityFrameworkProductBacklog : IProductBacklog { //Use EF here public bool ExistProductBacklogItem(string description) { //EF implementation throw new NotImplementedException(); } public bool ExistProductBacklogItem(int backlogItemId) { //EF implementation throw new NotImplementedException(); } public KeyValuePair<bool, int> TryAddProductBacklogItem(string description) { //EF implementation throw new NotImplementedException(); } public bool TryDeleteProductBacklogItem(int backlogItemId) { //EF implementation throw new NotImplementedException(); } } public class ControllerClientCode { private readonly IProductBacklog productBacklog; //Inject from Services, IoC, etc to unit test public ControllerClientCode(IProductBacklog productBacklog) { this.productBacklog = productBacklog; } public void AddProductBacklogItem(string description) { var businessRule = new AddProductBacklogItemBusinessRule(productBacklog); var generatedId = businessRule.Execute(description); //Do something with the generated backlog item id } public void DeletePRoductBacklogItem(int productBacklogId) { var businessRule = new DeleteProductBacklogItemBusinessRule(productBacklog); businessRule.Execute(productBacklogId); } } 
0
source

Source: https://habr.com/ru/post/1274272/


All Articles