문제

Let's say we have an aggregate root entity of type Order that relates customers and order lines. When I think about an order entity it's more natural to conceptualize it as not being defined without an Id. An order without an Id seems to be better represented as an order request than an order.

To add an order to a repository, I usually see people instantiate the order without the Id and then have the repository complete the object:

class OrderRepository
{
    void Add(Order order)
    {
        // Insert order into db and populate Id of new order
    }
}

What I like about this approach is that you are adding an Order instance to an OrderRepository. That makes a lot of sense. However, the order instance does not have an Id, and at the scope of the consumer of the repository, it still makes no sense to me that an order does not have an Id. I could define an OrderRequest to be an instance of order and add that to the repository, but that feels like deriving an apple from an orange and then adding it to a list of oranges.

Alternatively, I have also seen this approach:

class OrderRepository
{
    Order AddOrder(Customer customer)
        // It might be better to call this CreateOrder
    {
        // Insert record into db and return a new instance of Order
    }
}

What I like about this approach is that an order is undefined without an Id. The repository can create the database record and gather all the required fields before creating and returning an instance of an order. What smells here is the fact that you never actually add an instance of an order to the repository.

Either way works, so my question is: Do I have to live with one of these two interpretations, or is there a best practice to model the insertion?

I found this answer which is similar, but for value objects: how should i add an object into a collection maintained by aggregate root. When it comes to a value object there is no confusion, but my question concerns an entity with identiy derived from an external source (Auto-Generated Database Id).

도움이 되었습니까?

해결책

I would like to start by ruling out the second approach. Not only does it seem counter-intuitive, it also violates several good design principles, such as Command-Query Separation and the Principle of Least Surprise.

The remaining options depend on the Domain Logic. If the Domain Logic dictates that an Order without an ID is meaningless, the ID is a required invariant of Order, and we must model it so:

public class Order
{
    private readonly int id;

    public Order(int id)
    {
        // consider a Guard Clause here if you have constraints on the ID
        this.id = id;
    }
}

Notice that by marking the id field as readonly we have made it an invariant. There is no way we can change it for a given Order instance. This fits perfectly with Domain-Driven Design's Entity pattern.

You can further enforce Domain Logic by putting a Guard Clause into the constructor to prevent the ID from being negative or zero.

By now you are probably wondering how this will possibly work with auto-generated IDs from a database. Well, it doesn't.

There's no good way to ensure that the supplied ID isn't already in use.

That leaves you with two options:

  • Change the ID to a Guid. This allows any caller to supply a unique ID for a new Order. However, this requires you to use Guids as database keys as well.
  • Change the API so that creating a new order doesn't take an Order object, but rather a OrderRequest as you suggested - the OrderRequest could be almost identical to the Order class, minus the ID.

In many cases, creating a new order is a business operation that needs specific modeling in any case, so I see no problem making this distinction. Although Order and OrderRequest may be semantically very similar, they don't even have to be related in the type hierarchy.

I might even go so far as to say that they should not be related, because OrderRequest is a Value Object whereas Order is an Entity.

If this approach is taken, the AddOrder method must return an Order instance (or at least the ID), because otherwise we can't know the ID of the order we just created. This leads us back to CQS violation which is why I tend to prefer Guids for Entity IDs.

라이센스 : CC-BY-SA ~와 함께 속성
제휴하지 않습니다 StackOverflow
scroll top