作为第一步,以下 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();
}
}
}
简而言之,我们正在使用基础架构实现来提供并发安全性。