我是DDD新手并且对我的Aggregate Root的大小感到担忧。对象图如下图所示。 (他们是收藏品)。问题是所有实体都依赖于AggregateRoot(Event)的状态。我的问题是:如何将聚合分解为更小的聚合?就像我有一个"上帝"像聚合根只管理一切。
这是我的域名非常简单的视图:
这些是规则:
编辑:我已将1个聚合分成较小的聚合,以便事件,会话和照片都是AR。问题是会话需要在启动之前检查事件AR。将事件对象注入会话启动方法Session.Start(Event @event)是否完全可以,或者我是否会遇到一些注释中所述的并发问题?
答案 0 :(得分:1)
作为第一步,以下3篇文章将具有无可估量的价值:http://dddcommunity.org/library/vernon_2011/
使用DDD,您可以在从外部源完成单个操作(即方法调用)后将实体拆分到状态有效的边界。
根据您要解决的业务问题进行思考 - 您已经使用了删除一词......
删除是否在您正在为其设计系统的业务专家的措辞中占有一席之地?考虑现实世界而不是数据库基础设施,除非你能创建一个时间机器来回溯并阻止事件开始并因此改变历史,否则删除这个词没有现实世界的类比。
如果你强迫自己删除删除中的子节点,这意味着操作需要成为一个事务,因此也可能强制进入聚合根目录中的事情(以便实体的状态和所有一旦方法调用完成,它的子节点就可以被控制并保证有效。是的,您可以在多个聚合根中进行交易,但这些情况非常罕见,如果可能,应予以避免。
最终的一致性被用作交易的替代方案并降低复杂性,如果您与正在设计系统的人交谈,您可能会发现秒或分钟的延迟超出了可接受范围。这是充足的时间来发起一个事件,其他一些业务逻辑正在监听并采取必要的行动。使用最终一致性可以消除事务带来的麻烦。
照片可能占用大量存储空间,因此您可能需要在事件标记为已完成后运行的清理机制。一旦会话被标记为关闭,我可能会触发一个事件,其他地方的其他系统会监听此事件,并且在1年后(或任何对你有意义的事情)将其从服务器中移除...假设您使用了一个数组您的网址的字符串[10]。
如果这是您的业务逻辑的最大范围,那么不要只关注DDD,看起来这可能非常适合实体框架,实质上是CRUD并且内置了级联删除。
编辑回答
什么是照片,是否包含属性?是不是像照片的Url或图片文件的路径?
我还没有想到数据库,这应该是最后想到的事情,解决方案应该是数据库/技术无关的。我将规则视为:
您无法直接返回会话,因为您的代码的用户可能会在会话中调用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();
}
}
}
简而言之,我们正在使用基础架构实现来提供并发安全性。