Martin Fowler 的 POEAA 工作单元实施是 anti-pattern 吗?

Is Martin Fowler's POEAA implementation of Unit of Work an anti-pattern?

Martin Fowler 从 POEAA 一书中介绍了工作单元的概念。如果你想拥有 auto-commit 系统,它工作得很好,在这个系统中你的域模型使用工作单元将自己标记为新的、脏的、移除的或干净的。然后你只需要调用 UnitofWork.commit() ,模型的所有更改都会被保存。下面是一个领域模型 class 与这样的方法:

public abstract class DomainModel{

    protected void markNew(){
        UnitOfWork.getCurrent().registerNew(this);
    }

    protected void markDirty(){
        UnitOfWork.getCurrent().registerDirty(this);
    }

    protected void markRemoved(){
        UnitOfWork.getCurrent().registerRemoved(this);
    }        

    protected void markClean(){
        UnitOfWork.getCurrent().registerClean(this);
    }
} 

使用此实现,您可以通过业务逻辑方法将域模型标记为任何保存状态:

public class Message extends DomainModel{

    public void updateContent(User user, string content){
        // This method update message content if the the message posted time is not longer than 24 hrs, and the user has permission to update messate content.
        if(!canUpdateContent(user) && timeExpired()) throw new IllegalOperationException("An error occurred, cannot update content.");
        this.content = content;
        markDirty();
    }  
}

乍一看,它看起来很棒,因为您不必在 repository/data 映射器上手动调用插入、保存和删除方法。但是,我发现这种方法有两个问题:

  1. 域模型与工作单元的紧密耦合:这种工作单元的实现将使域模型依赖于 UnitOfWork class。 UnitOfWork 必须来自某个地方,static class/method 的实现很糟糕。为了改善这一点,我们需要切换到依赖注入,并将 UnitOfWork 的实例传递给领域模型的构造函数。但这仍然将领域模型与工作单元结合起来。同样理想情况下,域模型应该只接受其数据字段的参数(即消息域模型的构造函数应该只接受与消息相关的内容,例如标题、内容、发布日期等)。如果它需要接受 UnitOfWork 的参数,它会污染构造函数。

  2. 领域模型现在变成了persistent-aware:在现代应用程序设计中,尤其是DDD,我们力求persistent-ignorant模型。域模型不应该关心它是否被持久化,它甚至根本不应该关心是否有持久层。通过在域模型上使用那些 markNew()、markDirty() 等方法,我们的域模型现在有责任通知应用程序的其余部分它需要持久化。尽管它不处理持久性逻辑,但该模型仍然知道持久性层的存在。我不确定这是否是个好主意,对我来说这似乎违反了单一责任原则。还有一篇文章谈到这个: http://blog.sapiensworks.com/post/2014/06/04/Unit-Of-Work-is-the-new-Singleton.aspx/

那你怎么看? Martin Fowler 中描述的原始工作单元模式是否违反了良好的 OO 设计原则?如果是这样,您认为它是一种反模式吗?

我认为该模型不应依赖于 UoW。它更像是一个依赖于 UoW 的存储库,反过来,存储库将依赖于模型。

如果您的存储库仅依赖于抽象的 UoW,那么唯一了解持久性技术的难题就是具体的 UoW。

我倾向于允许模型依赖的唯一 类 是模型的其他部分:域服务、工厂等。

从 DDD 的角度来看,这是你不应该做的事情。

DDD 包含以下规则:

An application service should only modify one aggregate per transaction.

如果遵循此规则,则可以清楚在应用服务操作期间更改了哪个聚合。这个聚合然后又需要传递到存储库以保存到数据库:

repository.update(theAggregate);

不需要其他电话。这会抵消您描述的模式带来的好处。

另一方面,您描述的模式引入了从域到持久性机制的依赖性(取决于设计,是真正的依赖性还是概念性的依赖性)。 现在这是你应该避免的事情,因为它会大大增加你的模型的复杂性(不仅在内部,对于客户也是如此)。

因此,您不应将这种形式的模式与 DDD 一起使用。

DDD 之外

话虽如此,我认为该模式是解决某个问题的众多解决方案之一。该解决方案有利有弊,您在问题中描述了其中的一些。在某些情况下,模式可能是最好的权衡,所以

不,这不是反模式。

准确地说,没有人 "Martin Fowler's implementation of Unit of Work"。在书中,他区分了将修改后的对象注册到 UoW 的两种类型。

调用者注册 其中只有调用对象知道 UoW 并且必须将(被调用者)域对象标记为脏。 据我所知,这里没有反模式或不良做法

对象注册 其中域对象向 UoW 注册自己。这里同样有两个选项:

For this scheme to work the Unit of Work needs either to be passed to the object or to be in a well-known place. Passing the Unit of Work around is tedious but usually no problem to have it present in some kind of session object.

代码示例使用的是 UnitOfWork.GetCurrent(),它更接近后一种选择,并且由于紧密耦合、隐式依赖(服务定位器样式)而被广泛认为是当今的反模式。

但是,如果选择了第一个选项,即将 UoW 传递给域对象,并且让我们假设一个工作单元抽象,这会是不好的做法吗?从依赖管理的角度来看,显然不是。

现在仍然是执着无明面。我们可以说一个对象可以向另一个对象发出信号,它只是 edited/created/removed 它是 persistence-aware 吗?值得商榷。 相比之下,如果我们查看最近的领域对象实现,例如事件溯源中的实现,我们可以看到聚合 can be responsible for keeping a list of their own uncommitted changes 这或多或少是相同的想法。这是否违反了持久性无知?我不这么认为。

底线:Fowler 选择用于说明许多 UoW 可能性之一的特定代码现在显然被认为是不好的做法,但对于您指出的问题 #1 更是如此出来并不是真正的问题#2。这并没有取消他所写的其他实现的资格,也没有取消整个 UoW 模式的资格,其更改跟踪机制无论如何大部分时间都隐藏在第三方库魔术(阅读:ORM)中,而不是像书中的示例那样硬编码。