在2009年4月,我在MSDN发表了(“持久化模式”)文章,描述了一些当你使用ORM技术持久化业务对象时将会碰到的一些基本模式。我认为你和你的团队可能不会根据提纲实现自己的ORM工具,但是这些模式对于高效的使用(或者仅仅是选择)已存在的工具很重要。
在这篇文章中,我想用工作单元(Unit of Work)继续讨论持久化模式和审视围绕隐式持久化的问题。贯穿整篇文章的大部分,我使用一个泛化的发票系统作为问题域的例子。
工作单元(Unit of Work)模式
在企业软件设计中最常用的一个设计模式是工作单元。按照Martin Fowler的说法,工作单元模式是“维护一个被业务事务影响的对象列表,协调变化的写入和并发问题的解决。
工作单元模式并不需要你自己明确地构建,但是我注意到几乎每一个持久化工具中它都出现过。NHibernate中的ITransaction接口,LINQ to SQL中的DataContext类,还有Entity Framework中的ObjectContext类都是工作单元模式的例子。正是因为这个,古老的DataSet(venerable dataset)可以作为一个工作单元使用。
有时,你可能想根据使用的持久化工具写自己特定应用的工作单元接口或者类用于包装工作单元的内部逻辑。可能有很多原因这样做。你可能想为事物管理添加特定应用的日志,跟踪,或者错误处理。可能你想根据应用的剩余部分封装你的持久化工具的特定部分,想用这个额外的封装使以后替换持久化技术更加容易。或者你想促进你的系统的可测试程度。很多常用持久化工具的内部工作单元实现方式在自动化单元测试场景下很难处理。
如果你构建过你自己的工作单元实现,它可能与下面的接口相似:
public interface IUnitOfWork
{
void MarkDirty(object entity);
void MarkNew(object entity);
void MarkDeleted(object entity);
void Commit();
void Rollback();
}
你的工作单元类有一些方法能够标记对象是改变的,新的,或是删除的。(在很多实现中,MarkDirty方法不是必须的因为工作单元自身一些自动决定哪些实体被改变的方法。)工作单元也有一些方法用于提交或是回滚所有的改变。
在某种程度上,你可以认为工作单元是一个存放所有管理事务代码的地方。工作单元的职责有:
*管理事务。
*命令数据库插入,删除和更新。
*组织重复更新。在工作单元对象的单个用法中,代码的不同部分可能标记相同的发票 为已改变状态,但是工作单元类会只发送一个更新命令到数据库。
使用工作单元模式的价值在于其它代码不需要关心这些内容,因此你可以全神贯注业务逻辑。
使用工作单元
使用工作单元的一个最好方式是允许完全不同的类和服务参与到单一的逻辑事务中。这里的关键点是你要使完全不同的类和服务仍然彼此不感知,同时每一个在单个事务中有支持事务的能力。对于传统方式,你可能使用过事务协调器例如MTS/COM+,或者System.Transaction命名空间。就我个人而言,我更喜欢使用工作单元模式使不相关的类和服务参与到一个逻辑事务中,因为我认为这样使代码更清晰,更容易理解,并且更容易单元测试。
让我们假设你的新的发票系统在发票的生命周期里面的任何时间都会在已存在的发票上面执行各种离散的动作。业务也相当频繁的改变这些动作,因此你要频繁地添加或者删除新的发票动作,所以我们可以应用命令模式(”Simply Distributed System Design Using the Command Pattern, MSMQ, and .NET”)创建一个IInvoiceCommand的接口,它描述了一个作用于发票上的动作:
public interface IInvoiceCommand
{
void Execute(Invoice invoice, IUnitOfWork unitOfWork);
}
IInvoiceCommand接口有一个简单的Execute方法,它使用Invoice和IUnitOfWork执行不同类型的动作。任何实现IInvoiceCommand的对象都应该使用IUnitOfWork参数在在这个逻辑事务中持久化任何变化到数据库中。
足够简单,但是命令模式加工作单元模式并不会获得好处直到你把多个IInvoiceCommand对象放在一起(看图1)
图1 使用IInvoiceCommand
public class InvoiceCommandProcessor
{
private readonly IInvoiceCommand[] _commands;
private readonly IUnitOfWorkFactory _unitOfWorkFactory;
public InvoiceCommandProcessor(IInvoiceCommand[] commands, IUnitOfWorkFactory unitOfWorkFactory)
{
_commands = commands;
_unitOfWorkFactory = unitOfWorkFactory;
}
public void RunCommands(Invoice invoice)
{
IUnitOfWork unitOfWork = _unitOfWorkFactory.StartNew();
try
{
// Each command will potentially add new objects
// to the Unit of Work for insert, update, or delete
foreach (IInvoiceCommand command in _commands)
{
command.Execute(invoice, unitOfWork);
}
unitOfWork.Commit();
}
catch (Exception)
{
unitOfWork.Rollback();
}
}
}
通过工作单元的这种方法,你可以愉快地在你的发票系统中通过添加和删除业务规则混合和组合不同的IIncoiceCommand的实现,同时任然维持事务的完整性。
在我的经验中,业务人员似乎更关心晚的,未支付的发票,你可能将要必须添加新的IInvoiceCommand类,在一个发票被认为晚了的时候通过公司的代理商。下面是这个规则的可能实现方法:
public class LateInvoiceAlertCommand : IInvoiceCommand
{
public void Execute(Invoice invoice, IUnitOfWork unitOfWork)
{
bool isLate = isTheInvoiceLate(invoice);
if (!isLate) return;
AgentAlert alert = createLateAlertFor(invoice);
unitOfWork.MarkNew(alert);
}
}
这个设计的优美之处是LateInvoiceAlertCommand完全可以不依赖数据库进行开发和测试,甚至不依赖同一个事务中的其它IInvoiceCommand对象。首先,为了测试使用IUnitOfWork的IInvoiceCommand对象的交互,我可以创建一个IUnitOfWork的严格地伪实现保证测试的精确性,我可以调用StubUnitOfWork,一个记录stub。
public class StubUnitOfWork : IUnitOfWork
{
public bool WasCommitted;
public bool WasRolledback;
public void MarkDirty(object entity)
{
throw new System.NotImplementedException();
}
public ArrayList NewObjects = new ArrayList();
public void MarkNew(object entity)
{
NewObjects.Add(entity);
}
}
现在你得到一个独立于数据库可以运行的工作单元的伪实现,LaterInvoiceAlertCommand的测试设置可能类似于图2的代码:
图2 LaterInvoiceAlertCommand的测试固定设置(Test Fixture)
[TestFixture]
public class when_creating_an_alert_for_an_invoice_that_is_more_than_45_days_old
{
private StubUnitOfWork theUnitOfWork;
private Invoice theLateInvoice;
[SetUp]
public void SetUp()
{
// We're going to test against a "Fake" IUnitOfWork that
// just records what is done to it
theUnitOfWork = new StubUnitOfWork();
// If we have an Invoice that is older than 45 days and NOT completed theLateInvoice = new Invoice
{
InvoiceDate = DateTime.Today.AddDays(-50), Completed = false
};
// Exercise the LateInvoiceAlertCommand against the test Invoice new LateInvoiceAlertCommand().Execute(theLateInvoice, theUnitOfWork);
}
[Test]
public void the_command_should_create_a_new_AgentAlert_with_the_UnitOfWork()
{
// just verify that there is a new AgentAlert object
// registered with the Unit of Work theUnitOfWork.NewObjects[0].ShouldBeOfType();
}
[Test]
public void the_new_AgentAlert_should_have_XXXXXXXXXXXXX()
{
var alert = theUnitOfWork.NewObjects[0].ShouldBeOfType();
// verify the actual properties of the new AgentAlert object
// for correctness
}
}
原文链接:Patterns in Practice - The Unit Of Work Pattern And Persistence Ignorance