Как следует выражать добавление совокупного корня в хранилище? - PullRequest
5 голосов
/ 19 января 2010

Допустим, у нас есть совокупный корневой объект типа Order, который связывает клиентов и строки заказа. Когда я думаю о сущности заказа, более естественно представить ее как не определенную без идентификатора. Похоже, что заказ без идентификатора лучше представляется как запрос заказа, чем заказ.

Чтобы добавить заказ в хранилище, я обычно вижу, как люди создают заказ без идентификатора, а затем хранилище завершает объект:

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

Что мне нравится в этом подходе, так это то, что вы добавляете экземпляр Order в OrderRepository. Это имеет большой смысл. Однако экземпляр заказа не имеет идентификатора, и в контексте потребителя хранилища для меня все равно не имеет смысла, что заказ не имеет идентификатора. Я мог бы определить OrderRequest как экземпляр порядка и добавить его в хранилище, но это похоже на извлечение яблока из апельсина и затем добавление его в список апельсинов.

В качестве альтернативы я также видел такой подход:

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
    }
}

Что мне нравится в этом подходе, так это то, что заказ не определен без идентификатора. Хранилище может создать запись базы данных и собрать все необходимые поля перед созданием и возвратом экземпляра заказа. Здесь пахнет тот факт, что вы никогда не добавляете экземпляр заказа в хранилище.

В любом случае работает, поэтому мой вопрос: должен ли я жить с одной из этих двух интерпретаций, или есть ли лучшая практика для моделирования вставки?

Я нашел этот ответ, который похож, но для объектов значения: как добавить объект в коллекцию, поддерживаемую агрегатным корнем . Когда дело доходит до объекта значения, здесь нет путаницы, но мой вопрос касается сущности с идентификатором, полученным из внешнего источника (Auto-Generated Database Id).

1 Ответ

5 голосов
/ 19 января 2010

Я хотел бы начать с исключения второго подхода. Это не только кажется нелогичным, но и нарушает несколько хороших принципов проектирования, таких как Разделение команд-запросов и Принцип наименьшего сюрприза .

Остальные параметры зависят от логики домена. Если логика домена диктует, что Заказ без идентификатора не имеет смысла, идентификатор является обязательным инвариантом Заказа, и мы должны смоделировать его так:

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;
    }
}

Обратите внимание, что пометив поле id как readonly, мы сделали его инвариантом. Мы не можем изменить его для данного экземпляра Order. Это идеально подходит для доменно-управляемого дизайна Entity pattern.

Вы можете дополнительно применить доменную логику, поместив в конструктор предложение Guard, чтобы предотвратить отрицательный или нулевой идентификатор.

К настоящему времени вы, вероятно, задаетесь вопросом, как это будет работать с автоматически сгенерированными идентификаторами из базы данных. Ну, это не так.

Нет хорошего способа убедиться, что предоставленный идентификатор еще не используется.

Это оставляет вам два варианта:

  • Измените идентификатор на Guid. Это позволяет любому абоненту предоставить уникальный идентификатор для нового заказа. Однако для этого необходимо использовать Guids в качестве ключей базы данных.
  • Измените API, чтобы создание нового заказа не занимало объект Order, а скорее OrderRequest, как вы предложили - OrderRequest может быть практически идентичен классу Order, за исключением идентификатора.

Во многих случаях создание нового заказа является бизнес-операцией, которая в любом случае нуждается в конкретном моделировании, поэтому я не вижу проблем с проведением этого различия. Хотя Order и OrderRequest могут быть семантически очень похожими, они даже не должны быть связаны в иерархии типов.

Я мог бы даже пойти так далеко, чтобы сказать, что они не должны быть связанными, потому что OrderRequest является Value Object , тогда как Order является Entity .

Если выбран такой подход, метод AddOrder должен вернуть экземпляр Order (или, по крайней мере, ID), потому что в противном случае мы не можем знать ID заказа, который мы только что создали. Это возвращает нас к нарушению CQS, поэтому я склонен предпочесть Guids для идентификаторов сущностей .

...