我有以下实体:
> ConferenceSession : Entity
> - string Code
> - VenueId
>
> Venue : Entity
> - int MaxCapacity
>
> Attendee : Entity
业务要求是与会者可以注册一个或多个会议,反之亦然,一个会议可以有一个或多个与会者。
以下约束必须拒绝与会者注册:
哪个应该是合计根?我如何执行上述域约束,因为两个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;}
}
答案 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个会话。
以下是您可以检查的一些资源: