Подтвердить что ты не робот

Проект, управляемый доменом. Как получить доступ к дочернему элементу из заполнителя.

Если у меня есть класс Order как совокупный корень и 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";

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

4b9b3361

Ответ 1

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

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

Итак, Order.LineItems отлично, если он возвращает неизменяемую коллекцию (публично) неизменяемых объектов. Аналогично Order.LineItems[id]. Для примера см. источник для канонического утвержденного Evans примера ddd, где класс aggregate root Cargo предоставляет несколько своих дочерних элементов, но детские entites неизменны.

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

Если у вас есть "синяя книга" (Domain-Driven Design), см. пример на стр. 127, в котором показано, как вы можете Car.Engine, где оба Car и Engine являются совокупными корнями, но двигатель не является частью совокупности автомобилей, и вы не можете вносить изменения в движок, используя любой из методов Car (или наоборот -versa).

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

Ответ 2

Когда вы говорите, загрузите "Как загрузить только одну из 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. Вы также можете добавить методы методов для адреса, такие как 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 знал, где хранятся изменения в адресах.

Ответ 3

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

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

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