【问题标题】:Concerned about the size of my Aggregate Root [closed]担心我的聚合根的大小[关闭]
【发布时间】:2015-01-18 07:56:26
【问题描述】:

我是 DDD 新手,我担心聚合根的大小。对象图如下图所示。 (它们是集合)。问题是所有实体都依赖于 AggregateRoot (Event) 的状态。我的问题是:如何将聚合分解成更小的聚合?就像我有一个像聚合根一样的“上帝”,它只管理一切。

这是我的域的非常简单的视图:

这些是规则:

  • 一个事件有许多不同的状态。 (实现状态设计 模式在这里)。
  • 一个事件有一个会话集合。 (但一次只能激活 1 个,并且仅当事件处于正确状态时)。
  • 会话有两种状态:活动和结束。
  • 一个会话有一组客人。
  • 会话有一组照片。 (最大 10)。
  • 删除会话时。它应该删除它的所有孩子。
  • 当会话结束并删除照片时,它应该 检查是否有任何其他照片属于 会议。如果不是,它也应该删除会话。
  • 当会话结束并且照片被删除时,有时它应该根据事件的状态抛出异常。
  • 当会话处于活动状态并且照片被删除时。它不应该担心会话是否有任何其他照片。
  • 会话结束时,必须至少有 1 张照片和至少 1 位客人。
  • 可以更新照片,但前提是事件处于正确状态。
  • 删除事件后,应删除其所有子项。

编辑:我已将 1 个聚合划分为更小的聚合,因此 Event、Session 和 Photo 都是 AR。问题是会话需要在开始之前对事件 AR 执行检查。将事件对象注入会话启动方法 Session.Start(Event @event) 是否完全可以,或者我是否会遇到某些 cmets 中概述的并发问题?

【问题讨论】:

    标签: performance oop design-patterns domain-driven-design aggregate


    【解决方案1】:

    作为第一步,以下 3 篇文章将非常宝贵:http://dddcommunity.org/library/vernon_2011/

    使用 DDD,您可以将实体拆分为边界,在这些边界中,外部源的单个操作完成(即方法调用)后状态有效。

    从您要解决的业务问题的角度来思考 - 您经常使用删除这个词...

    删除甚至在您为其设计系统的业务专家的措辞中占有一席之地吗?从现实世界而不是数据库基础架构的角度思考,除非您可以创建一台时间机器来回到过去并阻止事件开始并因此改变历史,否则删除这个词没有现实世界的类比。

    如果您强制自己在删除时删除子项,则意味着该操作需要成为一个事务,因此位于聚合根中可能没有意义的事情也被强制执行(以便实体的状态和所有一旦方法调用完成,它的孩子就可以被控制并确保其有效)。是的,您可以对跨多个聚合根的事务执行某些操作,但这些情况非常罕见,应尽可能避免。

    最终一致性被用作事务的替代方案并降低复杂性,如果您与设计系统的人交谈,您可能会发现延迟几秒钟或几分钟是可以接受的。这是触发事件的充足时间,其他一些业务逻辑正在侦听该事件并采取必要的行动。使用最终一致性消除了事务带来的麻烦。

    是的,照片可能会占用大量存储空间,因此您可能需要一个在事件标记为完成后运行的清理机制。一旦会话被标记为关闭,我可能会触发一个事件,其他地方的不同系统会监听这个事件,并在 1 年后(或对你有意义的任何事情)从服务器中删除它......假设你使用了一个数组字符串 [10] 用于您的网址。

    如果这是您的业务逻辑的最大范围,那么不要只关注 DDD,这似乎很适合本质上是 CRUD 并内置了级联删除的实体框架。

    编辑答案

    什么是照片,它包含属性吗?它不是类似于照片的 URL 或图片文件的路径吗?

    我还没有考虑数据库,这应该是最后考虑的事情,并且解决方案应该与数据库/技术无关。我认为规则如下:

    • 一个事件有很多会话。
    • 会话具有以下状态:NotStarted、Started 和 Ended。
    • 一个会话有一组客人,我将假设这些客人是独一无二的(因为两个同名客人是不相同的,所以客人应该是一个聚合根)。
    • 一个事件有一个活动会话。
    • 当没有活动会话时,可以将事件标记为已完成。
    • 一旦事件被标记为已完成,就无法启动任何会话。
    • 一个会话最多包含 10 张照片。
    • 会话结束后,无法删除照片。
    • 如果没有客人,则无法开始会话如果没有照片,则无法结束会话。

    您不能直接返回 Session,因为您的代码用户可能会在会话上调用 Start(),您需要通过某种方式检查无法启动的事件,因此您可以链接到根这是为什么我将事件传递给 Session。如果您不喜欢这种方式,那么只需将操作 Session 的方法放在 Event 上(因此所有内容都通过 Event 访问,它执行所有规则)。

    在最简单的情况下,我将照片视为 Session 实体中的字符串(值对象)。作为第一次尝试,我会做这样的事情:

    // untested, do not know if will compile!
    public class Event
    {
        List<Session> sessions = new List<Session>();
    
        bool isEventClosed = false;
    
        EventId NewSession(string description, string speaker)
        {
            if(isEventClosed==true) 
                throw new InvalidOperationException("cannot add session to closed event");
    
            // create a new session, what will you use for identity, string, guid etc
            var sessionId = new SessionId(); // in this case autogenerate a guid inside this class
    
            this.sessions.Add(new Session(sessionId, description, speaker));
        }
    
        Session GetSession(EventId id)
        {
            reutrn this.sessions.FirstOrDefault(x => x.id == id);
        }   
    
        bool CanStartSession(Session session)
        {
            // TO DO: do a check session is in our array!!
            if(this.isEventClosed == true)
                return false;
    
            foreach(var session in sessions)
            {
                if(session.IsStarted()==true)
                    return false;
            }
            return true;
        }
    }
    
    public class Session
    {
        List<GuestId> guests = new List<GuestId>(); // list of guests
        List<string> photoUrls = new List<string>(); // strings to photo urls
        readonly SessionId id;
        DateTime started = null;
        DateTime ended = null;
        readonly Event parentEvent;
    
        public Session(Event parent, SessionId id, string description, string speaker)
        {
            this.id = id;
            this.parentEvent = parent;
            // store all the other params
        }
    
        void AddGuest(GuestId guestId)
        {
            this.guests.Add(guestId);
        }
    
        void RemoveGuest(GuestId guestId)
        {
            if(this.IsEnded())
                 throw new InvalidOperationException("cannot remove guest after event has ended");
        }
    
        void AddPhoto(string url)
        {
            if(this.photos.Count>10)
                throw new InvalidOperationException("cannot add more than 10 photos");
    
            this.photos.Add(url);
        }
    
        void Start()
        {
            if(this.guests.Count == 0)
                throw new InvalidOperationException("cant start session without guests");
    
            if(CanBeStarted())
                throw new InvalidOperationException("already started");
    
            if(this.parentEvent.CanStartSession()==false)
                throw new InvalidOperationException("another session at our event is already underway or the event is closed");
    
            this.started = DateTime.UtcNow;     
        }
    
        void End()
        {
            if(IsEnded()==true)
                throw new InvalidOperationException("session already ended");
    
            if(this.photos.length==0)
                throw new InvalidOperationException("cant end session without photos");
    
            this.ended = DateTime.UtcNow;
    
            // can raise event here that session has ended, see mediator/event-hander pattern
        }
    
        bool CanBeStarted()
        {
            return (IsStarted()==false && IsEnded()==false);
        }
    
        bool IsStarted()
        {
            return this.started!=null;
        }
    
        bool IsEnded()
        {
            return this.ended!=null;
        }   
    }
    

    对上述内容不提供任何保证,并且随着理解的发展以及您看到重构代码的更好方法可能需要随着时间的推移而改变。

    会话结束后无法移除访客 - 已通过简单测试添加此逻辑。

    谈论删除客人和离开 0 名客人的会话 - 您已声明一旦活动结束就不能删除客人......允许在任何时候发生这种情况将违反该业务规则,所以它永远不会发生,永远。此外,使用该术语删除问题空间中的人是没有意义的,因为人不能被删除,他们存在并且将永远有他们存在的记录。此数据库术语删除属于数据库,而不是您描述的此域模型。

    this.parentEvent.CanStartSession()==false 安全吗?不,它不是多线程安全的,但命令会独立运行,也许是并行运行,每个都在自己的线程中:

    void HandleStartSessionCommand(EventId eventId, SessionId sessionId)
    {
        // repositories etc, have been provided in constructor
        var event = repository.GetById(eventId);
        var session = event.GetSession(sessionId);
        session.Start();
        repository.Save(session);
    }
    

    如果我们使用事件溯源,那么在存储库中,它会在事务中写入已更改事件的流,并使用聚合根的当前版本,以便我们可以检测到任何更改。因此,就事件溯源而言,对 Session 的更改确实是对其父聚合根的更改,因为单独引用 Session 事件是没有意义的(它始终是一个 Event 事件,它不能独立存在)。显然,我在示例中给出的代码不是事件源代码,但可以这样编写。

    如果不使用事件溯源,则根据事务实现,您可以将命令处理程序包装在事务中作为横切关注点:

    public TransactionalCommandHandlerDecorator<TCommand>
        : ICommandHandler<TCommand>
    {
        private ICommandHandler<TCommand> decoratedHandler;
    
        public TransactionalCommandHandlerDecorator(
            ICommandHandler<TCommand> decoratedHandler)
        {
            this.decoratedHandler = decoratedHandler;
        }
    
        public void Handle(TCommand command)
        {
            using (var scope = new TransactionScope())
            {
                this.decoratedHandler.Handle(command);
                scope.Complete();
            }
        }
    }
    

    简而言之,我们正在使用基础架构实现来提供并发安全性。

    【讨论】:

    • 可以说我要让 Photo 成为它自己的聚合根。我可以做的是当一个事件被删除以触发一个最终将删除照片的域事件时。我对延迟没有问题……但这又引发了另外两个问题?如果在删除照片的情况下出现问题,会发生什么?我会在数据库中留下陈旧的数据吗? (它不再是交易)。另外我假设这意味着在数据库表 Photo 中,它必须对事件有一个空引用,以便在删除事件时它可以破坏引用。
    • 另外,如果 Photo 是它自己的聚合根,那么这意味着可以随时添加一个新的。防止这种情况发生的唯一方法是将事件聚合根注入照片的构造函数并执行检查。我假设这是不好的做法?顺便说一句,谢谢你的回复,我昨天确实看过那个页面。当我阅读它时我理解它,但我想将这些概念应用于您自己的特定问题会更困难。
    • 我唯一能想到的就是使用域服务来检查是否可以创建照片?
    • 已经包含了一些演示代码来展示几个示例来实现所需的规则
    • 感谢您的代码。规则似乎相对正确,但请记住,一个事件也有许多其他状态。它还缺少一条重要规则“一旦会话结束就不能删除来宾”现在这并不意味着来宾不能成为聚合根,因为它可以在会话完成时被删除,从而使会话留下 0 个来宾。在您的代码中,我假设 Event、Session 和 Photo 都是一个聚合的一部分,其中 Event 是 AR。如果是这种情况,在会话 End() 中引发事件的目的是什么?
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2010-12-30
    • 1970-01-01
    • 2012-06-27
    • 2013-12-07
    • 2018-05-23
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多