Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

C# / DDD: How to model entities with internal state objects not instantiable by the domain layer when using onion architecture?

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:

  • We could always use abstract factories for creating new entities, and those factories would then be implemented by the infrastructure. While there are certain cases where a domain factory is appropriate due to the complexity of the entity, I would not want to have to use one in every case as they lead to a lot of clutter in the domain and to yet another dependency being passed around.
  • Instead of directly using the database table wrappers as state objects, we could use another class (POCO) which just holds the values and then gets translated from/to database wrappers by the infrastructure. This might work but it would end up in a lot of additional mapping code and result in 3 or more classes per database table (DB wrapper, state object, domain entity) complicating maintenance. We would like to avoid this, if possible.
  • To avoid passing around factories, the constructor inside the entity could call some magic, singleton-like 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?

like image 219
Ben J Avatar asked Dec 22 '17 11:12

Ben J


1 Answers

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:

  • Try to avoid Orm´s(EF, Hibernate, etc) in your business rules because it adds the complexity of database(DataContext, DataSet,getters, setters, anemic models, code smells, etc) in business code.
  • In business rules use composition, the key is to inject through constructors the objects (the actors in the system), try to have purity in business rules.
  • Ask the object to do something with the data
  • Invest time in the design of the object API.
  • Leave implemetation details to the end(database, cloud, mongo, etc). You should implement the details in the class and dont let the complexity of the code spread outside it.
  • Try not to fit design patterns in your code always, only when needed.

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);
    }
}
like image 160
Elias Navarro Avatar answered Sep 20 '22 16:09

Elias Navarro