Проектирование на основе домена: как получить доступ к дочернему элементу совокупного корня - PullRequest
24 голосов
/ 20 января 2010

Если у меня есть класс Порядка в качестве совокупного корня и 1000 позиций.

Как загрузить только одну из 1000 позиций? Насколько я понимаю, позиция может быть доступна только через класс Order и имеет "локальную" идентичность. Буду ли я по-прежнему создавать метод репозитория в OrderRepository, например "GetLineItemById"?

Изменить, чтобы прокомментировать ответ: В настоящее время я не думаю, что разумно иметь неизменных детей. Что делать, если у меня есть класс Customer с несколькими адресами, контрактами и еще большим количеством дочерних коллекций. Огромная сущность, на которой я хочу выполнять методы CRUD.

Я бы получил

public class Customer
{
    public IEnumerable<Address> Addresses { get; private set; }
    public IEnumerable<Contracts> Contracts { get; private set; }
    ...
}

Должен ли я сделать что-то подобное, если пользователь исправит улицу адреса или свойство контракта?

public class Customer
{
    public void SetStreetOfAddress(Address address, street){}

    public void SetStreetNumberOfAddress(Address address, streetNumber){}
}

Тогда класс клиента будет полон дочерних методов манипуляции. Так что я бы предпочел сделать

addressInstance.Street = "someStreet";

Я думаю, что я неправильно понимаю всю концепцию ..:)

Ответы [ 3 ]

20 голосов
/ 20 января 2010

1) Нет ничего плохого в доступе к дочерним элементам совокупного корня через простые свойства только для чтения или методы get.

Важно убедиться, что все взаимодействия с детьми опосредованы совокупным корнем, так что существует единственное, предсказуемое место для гарантии инвариантов.

Так что Order.LineItems в порядке, пока он возвращает неизменную коллекцию (публично) неизменяемых объектов. Точно так же Order.LineItems[id]. Для примера см. источник канонического примера DDD, одобренного Эвансом , где совокупный корневой класс Cargo предоставляет несколько своих дочерних элементов, но дочерние объекты неизменны.

2) Совокупные корни могут содержать ссылки на другие совокупные корни, они просто не могут менять друг друга.

Если у вас есть «голубая книга» ( Домен-управляемый дизайн ), см. Пример на стр. 127, где показано, как у вас может быть Car.Engine, где Car и Engine являются агрегатными корнями, но двигатель не является частью агрегата автомобиля, и вы не можете вносить изменения в двигатель, используя любой из методов Car (или наоборот).

3) В доменном дизайне вам не нужно делать все ваши классы агрегатными корнями или потомками агрегатов. Вам нужны только совокупные корни для инкапсуляции сложных взаимодействий между сплоченной группой классов. Класс Customer, который вы предложили, звучит так, как будто он вообще не должен быть корнем агрегата - просто обычный класс, который содержит ссылки на агрегаты Contract и Address.

10 голосов
/ 21 января 2010

Когда вы говорите «загрузить» в «Как мне загрузить только одну из 1000 позиций?» Вы имеете в виду «загрузить из базы данных»? Другими словами, как мне загрузить только одну дочернюю сущность совокупного корня из базы данных?

Это немного сложно, но вы можете сделать так, чтобы ваши репозитории возвращали производную от агрегатного корня, чьи поля загружаются отложенным образом. Э.Г.

namespace Domain
{
    public class LineItem
    {
        public int Id { get; set; }
        // stuff
    }

    public class Order
    {
        public int Id { get; set; }

        protected ReadOnlyCollection<LineItem> LineItemsField;
        public ReadOnlyCollection<LineItem> LineItems { get; protected set; }
    }

    public interface IOrderRepository
    {
        Order Get(int id);
    }
}

namespace Repositories
{
    // Concrete order repository
    public class OrderRepository : IOrderRepository
    {
        public Order Get(int id)
        {
            Func<IEnumerable<LineItem>> getAllFunc = () =>
                {
                    Collection<LineItem> coll;
                    // { logic to build all objects from database }
                    return coll;
                };
            Func<int, LineItem> getSingleFunc = idParam =>
                {
                    LineItem ent;
                    // { logic to build object with 'id' from database }
                    return ent;
                };

            // ** return internal lazy-loading derived type **
            return new LazyLoadedOrder(getAllFunc, getSingleFunc);
        }
    }

    // lazy-loading internal derivative of Order, that sets LineItemsField
    // to a ReadOnlyCollection constructed with a lazy-loading list.
    internal class LazyLoadedOrder : Order
    {
        public LazyLoadedOrder(
            Func<IEnumerable<LineItem>> getAllFunc,
            Func<int, LineItem> getSingleFunc)
        {
            LineItemsField =
                new ReadOnlyCollection<LineItem>(
                    new LazyLoadedReadOnlyLineItemList(getAllFunc, getSingleFunc));
        }
    }

    // lazy-loading backing store for LazyLoadedOrder.LineItems
    internal class LazyLoadedReadOnlyLineItemList : IList<LineItem>
    {
        private readonly Func<IEnumerable<LineItem>> _getAllFunc;
        private readonly Func<int, LineItem> _getSingleFunc;

        public LazyLoadedReadOnlyLineItemList(
            Func<IEnumerable<LineItem>> getAllFunc,
            Func<int, LineItem> getSingleFunc)
        {
            _getAllFunc = getAllFunc;
            _getSingleFunc = getSingleFunc;
        }

        private List<LineItem> _backingStore;
        private List<LineItem> GetBackingStore()
        {
            if (_backingStore == null)
                _backingStore = _getAllFunc().ToList(); // ** lazy-load all **
            return _backingStore;
        }

        public LineItem this[int index]
        {
            get
            {
                if (_backingStore == null)        // bypass GetBackingStore
                    return _getSingleFunc(index); // ** lazy-load only one from DB **

                return _backingStore[index];
            }
            set { throw new NotSupportedException(); }
        }

        // "getter" implementations that use lazy-loading
        public IEnumerator<LineItem> GetEnumerator() { return GetBackingStore().GetEnumerator(); }

        IEnumerator IEnumerable.GetEnumerator() { return GetEnumerator(); }

        public bool Contains(LineItem item) { return GetBackingStore().Contains(item); }

        public void CopyTo(LineItem[] array, int arrayIndex) { GetBackingStore().CopyTo(array, arrayIndex); }

        public int Count { get { return GetBackingStore().Count; } }

        public bool IsReadOnly { get { return true; } }

        public int IndexOf(LineItem item) { return GetBackingStore().IndexOf(item); }

        // "setter" implementations are not supported on readonly collection
        public void Add(LineItem item) { throw new NotSupportedException("Read-Only"); }

        public void Clear() { throw new NotSupportedException("Read-Only"); }

        public bool Remove(LineItem item) { throw new NotSupportedException("Read-Only"); }

        public void Insert(int index, LineItem item) { throw new NotSupportedException("Read-Only"); }

        public void RemoveAt(int index) { throw new NotSupportedException("Read-Only"); }
    }
}

Абоненты OrderRepository.Get(int) получают что-то, что фактически является просто объектом Order, но на самом деле является LazyLoadedOrder. Конечно, для этого ваши совокупные корни должны предоставлять одного или двух виртуальных членов и быть рассчитаны вокруг этих точек расширения.

Изменить для обновления вопроса вопроса

В случае адреса я бы рассматривал его как объект значения, то есть неизменные композиции данных, которые вместе обрабатываются как одно значение.

public class Address
{
  public Address(string street, string city)
  {
    Street = street;
    City = city;
  }
  public string Street {get; private set;}
  public string City {get; private set;}
}

Затем, чтобы изменить агрегат, вы создаете новый экземпляр Address. Это аналогично поведению DateTime. Вы также можете добавить методы методов в Address, такие как SetStreet(string), но они должны возвращать новые экземпляры Address, так же как методы DateTime возвращают новые экземпляры DateTime.

В вашем случае объекты неизменяемого значения адреса должны сочетаться с каким-либо наблюдением за коллекцией адресов. Простой и понятный метод - отслеживать добавленные и удаленные AddressValues ​​в отдельных коллекциях.

public class Customer
{
    public IEnumerable<Address> Addresses { get; private set; }

    // backed by Collection<Address>
    public IEnumerable<Address> AddedAddresses { get; private set; } 

    // backed by Collection<Address>
    public IEnumerable<Address> RemovedAddresses { get; private set; }

    public void AddAddress(Address address)
    {
      // validation, security, etc
      AddedAddresses.Add(address);
    }

    public void RemoveAddress(Address address)
    {
      // validation, security, etc
      RemovedAddresses.Add(address);
    }

    // call this to "update" an address
    public void Replace(Address remove, Address add)
    {
      RemovedAddresses.Add(remove);
      AddedAddresses.Add(add);
    }
}

В качестве альтернативы вы можете добавить адрес с ObservableCollection<Address>.

Это действительно чистое решение DDD, но вы упомянули NHibernate. Я не эксперт NHibernate, но я думаю, что вам придется добавить код, чтобы NHibernate знал, где хранятся изменения в адресах.

1 голос
/ 21 января 2010

Когда вам нужно получить доступ к дочерней сущности по Id, она превращает дочернюю сущность в совокупный корень. Нет ничего плохого в том, что у агрегатных корней есть другие агрегатные корни, как у детей, или даже у детей со ссылкой на родителя. Отдельный репозиторий для дочерней сущности в порядке. Когда агрегированные корни содержат агрегированные корни, мы должны помнить о концепции «ограниченного контекста», чтобы предотвратить объединение слишком больших частей домена и затруднить изменение кода. Когда это происходит, причина состоит в том, что большую часть времени совокупные корни вкладываются вглубь. Это не должно быть проблемой в вашем случае, вложение строк в порядке звучит очень разумно.

Чтобы ответить на вопрос, должны ли вы вкладывать позиции, у меня есть теперь, почему вы хотите загружать позиции по идентификатору, и продажа 1000 позиций за заказ звучит так, как будто приложение будет продавать много?

Когда вы вкладываете позиции в заказ и ожидаете, что в заказах будет много позиций, вы можете посмотреть на несколько вариантов отображения / кэширования / загрузки запросов, чтобы заставить большие заказы работать так, как это требуется приложению. , Ответ о том, как наиболее быстро загрузить нужные позиции, зависит от контекста, в котором вы его используете.

...