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 映射器上手动调用插入、保存和删除方法。但是,我发现这种方法有两个问题:
域模型与工作单元的紧密耦合:这种工作单元的实现将使域模型依赖于 UnitOfWork class。 UnitOfWork 必须来自某个地方,static class/method 的实现很糟糕。为了改善这一点,我们需要切换到依赖注入,并将 UnitOfWork 的实例传递给领域模型的构造函数。但这仍然将领域模型与工作单元结合起来。同样理想情况下,域模型应该只接受其数据字段的参数(即消息域模型的构造函数应该只接受与消息相关的内容,例如标题、内容、发布日期等)。如果它需要接受 UnitOfWork 的参数,它会污染构造函数。
领域模型现在变成了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)中,而不是像书中的示例那样硬编码。
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 映射器上手动调用插入、保存和删除方法。但是,我发现这种方法有两个问题:
域模型与工作单元的紧密耦合:这种工作单元的实现将使域模型依赖于 UnitOfWork class。 UnitOfWork 必须来自某个地方,static class/method 的实现很糟糕。为了改善这一点,我们需要切换到依赖注入,并将 UnitOfWork 的实例传递给领域模型的构造函数。但这仍然将领域模型与工作单元结合起来。同样理想情况下,域模型应该只接受其数据字段的参数(即消息域模型的构造函数应该只接受与消息相关的内容,例如标题、内容、发布日期等)。如果它需要接受 UnitOfWork 的参数,它会污染构造函数。
领域模型现在变成了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)中,而不是像书中的示例那样硬编码。