关注我的聚合根的大小

时间:2014-11-20 12:40:30

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

我是DDD新手并且对我的Aggregate Root的大小感到担忧。对象图如下图所示。 (他们是收藏品)。问题是所有实体都依赖于AggregateRoot(Event)的状态。我的问题是:如何将聚合分解为更小的聚合?就像我有一个"上帝"像聚合根只管理一切。

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

enter image description here

这些是规则:

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

编辑:我已将1个聚合分成较小的聚合,以便事件,会话和照片都是AR。问题是会话需要在启动之前检查事件AR。将事件对象注入会话启动方法Session.Start(Event @event)是否完全可以,或者我是否会遇到一些注释中所述的并发问题?

1 个答案:

答案 0 :(得分:1)

作为第一步,以下3篇文章将具有无可估量的价值:http://dddcommunity.org/library/vernon_2011/

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

根据您要解决的业务问题进行思考 - 您已经使用了删除一词......

删除是否在您正在为其设计系统的业务专家的措辞中占有一席之地?考虑现实世界而不是数据库基础设施,除非你能创建一个时间机器来回溯并阻止事件开始并因此改变历史,否则删除这个词没有现实世界的类比。

如果你强迫自己删除删除中的子节点,这意味着操作需要成为一个事务,因此也可能强制进入聚合根目录中的事情(以便实体的状态和所有一旦方法调用完成,它的子节点就可以被控制并保证有效。是的,您可以在多个聚合根中进行交易,但这些情况非常罕见,如果可能,应予以避免。

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

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

如果这是您的业务逻辑的最大范围,那么不要只关注DDD,看起来这可能非常适合实体框架,实质上是CRUD并且内置了级联删除。

编辑回答

什么是照片,是否包含属性?是不是像照片的Url或图片文件的路径?

我还没有想到数据库,这应该是最后想到的事情,解决方案应该是数据库/技术无关的。我将规则视为:

  • 活动有很多会话。
  • 会话具有以下状态:NotStarted,Started和Ended。
  • 会话有一组来宾,我将假设它们是唯一的(因为两个同名的客人不一样,所以来宾应该是聚合根)。
  • 一个活动有一个活跃的会话。
  • 如果没有活动的会话,则可以将事件标记为已完成。
  • 一旦事件被标记为已完成,就无法启动会话。
  • 会话最多可收集10张照片。
  • 会话结束后,无法删除照片。
  • 如果没有访客,则会话无法启动如果没有照片,则会话无法结束。

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

在最简单的情况下,我在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();
        }
    }
}

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