DDD双向解决业务域验证中的多对多问题

时间:2019-06-12 00:18:30

标签: domain-driven-design

我有以下实体:

> ConferenceSession : Entity
> - string Code 
> - VenueId
> 
> Venue : Entity
> - int MaxCapacity
> 
> Attendee : Entity

业务要求是与会者可以注册一个或多个会议,反之亦然,一个会议可以有一个或多个与会者。

以下约束必须拒绝与会者注册:

  • 与会者不能注册5个以上的会议
  • 会议会话的参加人数不能超过最大容纳人数

哪个应该是合计根?我如何执行上述域约束,因为两个ConferenceSession都需要链接与会者,并且与会者都需要链接ConferenceSession?

我知道这里可能会问类似的问题(many to many relationship in ddd),但是它没有任何约束,因此可以一对多进行。

到目前为止,我已经提出了以下建议:

Class Attendee : AggreateRoot
{
     Registration[] Registrations { get; }
     void Register(ConferenceSession session){
          if (this.Registrations.Count >= 5){ throw domainexception; }
          if (!session.CanRegister()){ throw domainexception; }

          // Do i do "Registrations.Add(new Registration)" here ? what about the Registrations in ConferenceSession ? 
     }
}

Class ConferenceSession : Entity
{
     Registration[] Registrations { get; }
     int Capacity { get; }

     bool CanRegister()
     {
          return this.Registrations.Count < this.Capacity;
     }
}

Class Registration : Entity
{
     Registration(ConferenceSession session, Attendee attendee)
     {
         this.Session = session;
         this.Attendee = attendee; 
     }

     ConferenceSession Session {get;}
     Attendee Attendee {get;}


}

1 个答案:

答案 0 :(得分:0)

在模型中具有多对多关系是正常的。在模型中的概念之间存在约束也是正常的。

这并不意味着必须使用以下命令来完成 model implementation 对象从一个实体到另一个实体的引用*。同样,关联不必是双向的。

在具有跨多个聚合的操作且此操作更为复杂的情况下,您具有 Process 。实现 Processes 的一种方法是使用Saga

以下是需要回答的几个问题:

  • 您如何识别参加者?您是否使用名称或某种代码的电子邮件?想要参加会议的人在注册会议之前会创建帐户吗?

  • 如果所有会议都已满,那么请求成为与会者的人是否应该拒绝其 Request ? >

我认为您可能在模型中缺少某些概念,或者您没有完全解释它。考虑到上述问题,我将根据一些假设给出一个域模型的示例。

让我们说一个人必须用他的电子邮件创建一个帐户,以便可以识别他/她。

创建帐户后,此人可以尝试注册 ConferenceSessions 。如果没有任何席位可供任何会议使用,则此人将拥有一个帐户,而根本无法成为参加者

这将简化逻辑,因为当某人尝试参加 ConferenceSession 时,我们不必第一次进行验证。它还将使我们能够识别一个人(使用他/她的帐户)。拥有不参加任何活动的帐户的开销很大,但是如果这是一个大型系统,人们以后可以参加其他会议,他们将可以再次使用其帐户。此外,我们的系统将能够跟踪此人。参加了多少次会议,依此类推。此帐户也可以关联信用卡或其他付款方式。

好的,所以当一个人注册时,他会创建一个帐户帐户仅具有不涉及其他实体的唯一电子邮件约束,因此我将跳过这一部分并集中精力 在其他多对多关系上。

注意:地点具有最大容量,但是您谈论的ConferenceSessions具有一定的容量,因此我仅假设这些会议具有容量,而忽略不清楚的地点。这不会改变所提供解决方案的有效性。

然后,一个人可以使用其帐户尝试注册 ConferenceSession 。根据 ConferenceSession 和当前与会者的能力,可以批准拒绝他的 RegistrationRequest 计数以及他参加了多少次会议。

此外,我们的系统中还发生了一些事件:

  • JoinSessionRequested
  • JoinSessionRequestRejected
  • JoinSessionsRequestWaitingForApproval
  • JoinSessionRequestApproved
  • ConferenceSessionJoined
  • JoinConferenceSessionRejected

我们将创建一个反应性系统,该系统将使用这些事件来触发操作。

这是一种可能的实现方式。

public class Account : ConcurrentEntity {

    public Guid ID { get; }
    public Email Email { get; }
}

public class Attendee : ConcurrentEntity {

    public Guid AccountID { get; }
    public Guid ConferenceSessionID { get; }
}

public class ConferenceSession : ConcurrentEntity {

    public Guid ID { get; }
    public Guid ConferenceID { get; }
    public Attendee[] Attendees { get; }
    public Capacity MaxCapacity { get; }

    public bool HasReachedMaxCapacity {
         get { return Attendees.Length == MaxCapacity; }
    }

    public void RegisterAttendee(Guid accountID) {

        if(HasReachedMaxCapacity) {
            throw new Exception("Session has reached max capacity");
        }

        Attendees.Add(new Attendee(this.ID, accountID));
    }
}

public enum RequestStatus { Pendind, WaitingApproval, Approved, Rejected }

public class JoinConferenceSessionRequest {

    public Guid ID { get; }
    public RequestStatus Status { get; ]
    public Guid AccountID{ get; }
    public Guid SessionID { get; }

    public void Accept() {
        Status = RequestStatus.Accepted;
        AddEvent(new JoinSessionsRequestAccepted(
            this.ID, this.UserID, this.SeesionID));
    }

    public void Reject() {
        Status = RequestStatus.Rejected;
        AddEvent(new JoinSessionsRequestRejected(
            this.ID, this.UserID, this.SeesionID));
    }

    public void TransitionToWaitingForApprovalState() {
        Status = RequestStatus.WaitingForApproval;
        AddEvent(new JoinSessionsRequestWaitingForApproval(
            this.ID, this.UserID, this.SeesionID));
    }
}

public class RequestToJoinConferenceSessionCommand {

    public void Execute(Guid accountID, Guid conferenceSeesionID) {

        var request = new JoinConferenceSessionRequest(
            accountID, conferenceSessionID);

        JoinConferenceSessionRequestRepository.Save(request);
    }
}

public class JoinSessionRequestedEventHandler {

    public void Handle(JoinSessionRequestedEvent event) {

        var request = JoinConferenceSessionRequestRepository
            .GetByID(event.RequestID);

        bool hasAlreadyRequestedToJoinThisSession = 
                JoinConferenceSessionRequestRepository
                    .ExistsForConfernce(accountID, conferenceSessionID);

        // CONSTRANT: THE USER CANNOT REQUEST TO JOIN THE 
        // SAME SESSIONS TWO TIMES, tHIS CAN HAPPEN BECAUSE NO ONE STOPS THE 
        // USER FROM OPENING TWO BROWSERS/MOBILE APPS OR BROWSER AND MOBILE

        if(hasAlreadyRequestedToJoinThisSession) { 
            request.Reject();
        }
        else {
            var acceptedUserRequestsCount = 
                 JoinConferenceSessionRequestRepository
                   .GetAcceptedRequestsCountForUser(event.UserID);

            // CONSTRAIN: A USER CANNOT JOIN MORE THAN 5 SESSION.
            // BECAUSE REQUESTS ARE MADE EXPLICITLY WE CAN COUNT HOW MANY 
            // ACCEPTED REQUESTS HE HAS AT THIS POINT IN TIME. iF HE HAS                   
            // MORE THAN 5, WE REJECT THE REQUEST

            if(acceptedUserRequestsCount > 5) {
                request.Reject();
            }
            else {
                request.TransitionToWaitingForApprovalState();
            }
        }

        JoinConferenceSessionRequestRepository.Save(request);
    }
}

public class JoinSessionsRequestWaitingForApprovalEventHandler {

    public void Handle(JoinSessionsRequestWaitingForApproval event) {

        var session = ConferenceSessionRepository.GetByID(event.SessionID);
        var account = AccountRepository.GetByID(event.AccountID);

        // CONSTRAINT: THE USER CANNOT REGISTER FOR THE SESSION IF IT HAS  
        // REACHED IT'S CAPACITY. IF IT HAS WE NEED TO PUBLISH AN EVENT TO 
        // NOTIFY THE REST OF THE SYSTEM FOR THAT                          
        // SO THE REQUEST CAN BE REJECTED

        if (session.HasReachedMaxCapacity) {
            MessageBus.PublishEvent(
                new JoinConferenceSessionRejected(session.ID, account.ID);
        }
        else {
            session.RegisterAttendee(account);
            ConferenceSessionRepository.Save(session);
        }
    }
}

public class JoinConferenceSessionRejectedEventHandler {

    public void Handle(JoinConferenceSessionRejectedEvent event) {

        var request = ConferenceSessionRequestRepository
            .FindForUserAndSession(event.UserID, event.SessionID);

        request.Reject();

        ConferenceSessionRequestRepository.Save(request);
    }
}

public class ConferenceSessionJoinedEventHandler {

    public void Handle(ConferenceSessionJoinedEventHandler event) {

        var request = ConferenceSessionRequestRepository
            .FindForUserAndSession(event.UserID, event.SessionID);

        request.Accept();

        ConferenceSessionRequestRepository.Save(request);
    }
}

在此解决方案中请注意,我们仅使用事件。验证失败时,事件也用于通知。我们不抛出和处理异常。我们捕获了系统中的事件的协议中可能发生的所有事情。

与会者以外的所有实体均为聚合根。与会者是 ConferenceSession 聚合中包含的一个实体,可以更轻松地实施规则。我们还使用在 ConcurrentEntity 基类中实现的Optimistic Offline lock。我们还使用 ID引用代替了对象引用

我们记录了一个帐户加入会话的所有请求,因此我们可以强制执行约束或只有一个人参加5个会话。

以下是您可以检查的一些资源: