1

我正在阅读 Vaughn Vernon 关于实现领域驱动设计的书。我也从他的 github here 浏览了本书的代码,C# 版本

这本书的Java版本有装饰器@Transactional,我相信它来自spring框架。

public class ProductBacklogItemService
{
    @Transactional
    public void assignTeamMemberToTask(
        string aTenantId,
        string aBacklogItemId,
        string aTaskId,
        string aTeamMemberId)
        {
            BacklogItem backlogItem =
                backlogItemRepository.backlogItemOfId(
                    new TenantId(aTenantId),
                    new BacklogItemId(aBacklogItemId));

            Team ofTeam =
                teamRepository.teamOfId(
                    backlogItem.tennantId(),
                    backlogItem.teamId());

            backlogItem.assignTeamMemberToTask(
                new TeamMemberId(aTeamMemberId),
                ofTeam,
                new TaskId(aTaskId));
        }
}

C# 中等效的手动实现是什么?我在想一些事情:

public class ProductBacklogItemService
{
    private static object lockForAssignTeamMemberToTask = new object();
    private static object lockForOtherAppService = new object();

    public voice AssignTeamMemberToTask(string aTenantId,
        string aBacklogItemId,
        string aTaskId,
        string aTeamMemberId)
        {
            lock(lockForAssignTeamMemberToTask)
            {
                // application code as before
            }
        }

        public voice OtherAppsService(string aTenantId)
        {
            lock(lockForOtherAppService)
            {
                // some other code
            }
        }
}

这给我留下了以下问题:

  1. 我们是按应用程序服务锁定还是按存储库锁定?即我们不应该这样做backlogItemRepository.lock()吗?
  2. 当我们将多个存储库作为应用程序服务的一部分读取时,我们如何在事务期间保护存储库之间的依赖关系(聚合根通过身份引用其他聚合根)——我们是否需要在存储库之间建立互连锁?
  3. 是否有任何 DDD 基础架构框架可以处理任何此类锁定?

编辑

使用事务有两个有用的答案,因为我没有选择我的持久层我正在使用内存存储库,这些非常原始并且我编写了它们(它们没有事务支持,因为我不知道如何添加!)。

我将设计系统,因此我不需要同时对多个聚合根进行原子更改,但是我需要在多个存储库中一致地读取(即,如果 BacklogItemId 从多个其他聚合中引用,那么如果 BacklogItemId 被删除,我们需要防止竞争条件)。

那么,我可以只使用锁,还是需要考虑在我的内存存储库中添加 TransactionScope 支持?

4

4 回答 4

5

TL;DR 版本

您需要将代码包装在System.Transactions.TransactionScope. 小心多线程顺便说一句。

完整版本

所以聚合的重点是定义一致性边界。这意味着任何更改都应该导致聚合的状态仍然尊重它的不变量。这不一定与交易相同。真正的事务是一个横切的实现细节,所以可能应该这样实现。

关于锁定的警告

不要锁定。试着忘记你对实现悲观锁定的任何想法。要构建可扩展系统,您别无选择。数据需要时间来请求并从磁盘传输到屏幕这一事实意味着您具有最终的一致性,因此您应该为此构建。您不能真正防止竞争条件,您只需要考虑它们可能发生的事实并能够警告“失败”用户他们的命令失败。通常,您可以稍后开始发现这些问题(几秒钟、几分钟、几小时、几天,无论您的领域专家告诉您 SLA 是什么)并告诉用户,以便他们可以采取一些措施。

例如,想象一下如果两名工资职员同时在银行支付员工的费用。他们稍后会发现账簿平衡的时间,并采取一些补偿措施来纠正这种情况。为了避免这些(罕见的)问题,您不希望将您的工资部门缩减为一次工作的一个人。

我的实现

我个人使用命令处理器样式,所以我所有的应用程序服务都实现为ICommandHandler<TCommand>. 它CommandProcessor本身就是查找正确的处理程序并要求它处理命令的东西。这意味着该CommandProcessor.Process(command)方法可以在System.Transactions.TransactionScope.

例子:

public class CommandProcessor : ICommandProcessor
{
    public void Process(Command command)
    {
        using (var transaction = new TransactionScope())
        {
            var handler = LookupHandler(command);
            handler.Handle(command);

            transaction.Complete();
        }
    }
}

您还没有采用这种方法,因此要使您的事务成为横切关注点,您需要将它们移到堆栈中更高的级别。这高度依赖于您使用的技术(ASP.NET、WCF 等),因此如果您添加更多细节,则可能有一个明显的地方可以放置这些东西。

于 2013-11-16T21:09:51.657 回答
4

锁定不允许这些代码路径上的任何并发。

我认为您正在寻找事务范围

于 2013-11-15T10:12:36.443 回答
1

我不知道您将使用哪个持久层,但标准的 ADO.NET、Entity Framework 等支持 TransactionScope 语义:

using(var tr = new TransactionScope())
{
    doStuff();
    tr.Complete();
}

如果调用 tr.Complete(),则提交事务。在任何其他情况下,它都会回滚。

通常,聚合是事务一致性的单位。如果您需要事务分布在多个聚合中,那么您可能应该重新考虑您的模型。

于 2013-11-15T13:46:36.343 回答
-1
lock(lockForAssignTeamMemberToTask)
{
    // application code as before
}

这负责同步。但是,如果出现任何异常,您还需要还原更改。因此,模式将类似于:

lock(lockForAssignTeamMemberToTask)
{
    try {
        // application code as before
    } catch (Exception e) {
        // rollback/restore previous values
    }
}
于 2013-11-14T09:30:59.063 回答