[.net] 领域驱动的设计:如何访问聚合根的孩子


Answers

当您在“如何仅加载 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)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的新实例一样。

在你的情况下,不可变的Address值对象必须与Addresses集合的某种观察相结合。 一个简单而干净的技术是跟踪添加和删除单独集合中的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知道地址的变化被存储在哪里。

Question

如果我有一个订单类作为聚合根和1000行项目。

如何只加载1000个订单项中的一个? 据我所知,订单项只能通过Order类访问,并具有“本地”身份。 我仍然会在OrderRepository中创建一个存储库方法,如“GetLineItemById”?

编辑评论答案:目前我不认为有一个不可改变的孩子是合理的。 如果我有一个客户类与几个地址,合同,甚至更多的儿童收藏。 一个庞大的实体,我想执行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";

我想我是误解了整个概念.. :)