I am in the process of migrating a "big ball of mud" (BBOM)-like system towards a system based on the ideas of domain driven design.
After various iterations of refactoring, domain aggregates/entities are currently modelled using inner state objects, as described by Vaughn Vernon in this article, for example: https://vaughnvernon.co/?p=879#comment-1896
So basically, 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;
[...]
}
As of today, the state object in this system is always a database table wrapper coming from the currently used proprietary data access framework of the application, which resembles an Active Record pattern. All the state objects therefore inherit from a base class part of the data access framework. At this time, it is not possible to use POCOs as state object, Entity Framework or any of that.
The application currently uses a classic layer architecture in which the infrastructure (including the mentioned table wrappers / state objects) is at the bottom, followed by the domain. The domain knows the infrastructure and the repositories are implemented in the domain, using the infrastructure. As you can see above, most entities contain a public constructor for conveniently creating new instances inside the domain, which internally just creates a new state object (because the domain knows it).
Now, we would like to further evolve this and gradually turn the architecture around, resulting more in an "onion" kind of architecture. In that architecture, the domain would only contain repository interfaces, and the actual implementations would be provided by the infrastructure layer sitting on top of it. In this case, the domain could no longer know the actual state objects / database table wrappers.
One idea to solve this would be to have the state objects implement interfaces defined by the domain, and this actually seems like a good solution for now. It is also technically possible because, even though the state objects must inherit from a special data access base 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) instantiates a new Customer, it can easily pass in the database wrapper object which implements the ICustomerState. So far so good
However, when creating new entities in the domain, it is no longer possible to also create the inner state object as we no longer know the actual implementation of it.
There are several possible solutions to this, but none of them seem really attractive:
StateFactory.Instance.Create<TState>()
method for creating the inner state object. It would then be the infrastructure's responsibility to register an appropriate implementation for it. A similar approach would be to somehow get the DI container and resolve the factory from there. I personally don't really like this sort of Service Locator approach but it might be acceptable in this special case.Are there any better options that I'm missing?
Domain driven design is not a god fit for big ball of muds. Trying to apply DDD in big systems is not as effective as object oriented desing. Try to think in terms of object that collaborate together and hide the complexity of data and start thinking in methods/behavior to manipulate the object internals through behavior.
In order to achieve onion arquitecture i would suggest the following rules:
Here is how i would design business rules with objects in order to have 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);
}
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With