DDD删除聚合的最佳解决方案

DDD Best solution to delete aggregate

在 DDD 中我有一个聚合 Lobby

public class Lobby : Entity
{
    public List<User> Users { get; private set; }

    public void RemoveUser(string userId) 
    {
        var user = Users.First(e => e.Id == userId);
        Users.Remove(user);
        if(Users.Count == 0)
        {
            // REMOVE THIS LOBBY
        }   
    }
}

和应用服务

public class LobbyService : ILobbyService
{
    public void RemoveUser(string lobbyId, string userId)
    {
        var lobby = _lobbyRepository.GetById(lobbyId);
        lobby.RemoveUser(userId);
        _lobbyRepository.SaveChanges();
    }
}

删除该大厅的最佳方法是什么?抛出异常/返回特定值或域事件?然而,前一种解决方案意味着我的应用程序服务将使用一堆 if 语句(我认为这很糟糕?),而后者将意味着将有两次数据库往返(从事件处理程序中删除,然后从应用程序服务更新,因为 dbContext不会分享) 我是 DDD 的新手,但我还不是很清楚。

我个人仍然会使用 if statement 来检查 lobby 是否为 null。

public class LobbyService : ILobbyService
{
    public void RemoveUser(string lobbyId, string userId)
    {
        var lobby = _lobbyRepository.GetById(lobbyId);

        if (lobby == null)
            throw new ArgumentNullException("Lobby is null"); // or return Result.Error("Lobby is null");
        
        lobby.RemoveUser(userId);
        
        _lobbyRepository.SaveChanges();

        UserRemoved(lobbyId, userId);
    }
}

"Don't Delete, Just Don't" -- Udi Dahan, 2009

就是说,如果您要删除某些内容...

存储库的操作通常发生在应用程序组件中;换句话说,当实体处于特定状态时,您想调用 delete 而不是保存。

public void RemoveUser(string lobbyId, string userId)
{
    var lobby = _lobbyRepository.GetById(lobbyId);
    lobby.RemoveUser(userId);

    if(ShouldDelete(lobby)) {
        _lobbyRepository.delete(lobbyId)
    }

    _lobbyRepository.SaveChanges();
}

boolean ShouldDelete(Lobby lobby)
{
    return ????
}

the former solution would mean my Application service would use a bunch of if statements (which I think is bad?)

不,这还不错 - 这不是常见情况。

关键思想:应用逻辑和领域逻辑是不同的动物。应用逻辑属于应用程序,领域逻辑属于领域模型。 “我们应该使用哪个 I/O 操作?”是应用程序问题,而不是领域问题。


更常见的方法是将删除安排在以后发生(异步)。有很多不同的方法可以做到这一点——最简单的是让领域实体记录它自己的生命终结,然后在稍后的某个时间 运行 一个实际执行回收过程的“垃圾收集器”未使用的 space.

在那种设计中,您的应用程序代码看起来更像您期望的那样:

public void RemoveUser(string lobbyId, string userId)
{
    var lobby = _lobbyRepository.GetById(lobbyId);
    lobby.RemoveUser(userId);
    _lobbyRepository.SaveChanges();
}

异步垃圾收集器类似于

DELETE FROM lobby WHERE lobby.should_delete

should_delete 可能是一个布尔值,或者它可能是一个时间戳,用于与 now() 或某个目标时间等进行比较。许多可能性,不同的权衡。


可以使用消息传递与垃圾收集器通信 - 您会得到一组不同的权衡。消息是 I/O 的另一种形式,因此应用程序将对它们有所了解 - 但它可能是一种非常通用且可重复使用的机制。

同样,许多权衡 - 消息是持久的还是短暂的?是数据库事务的一部分?我们的消息传输解决方案有多可靠,我们是否需要备份机制来确保即使删除消息丢失也能删除大厅?