模式实践之工作单元【翻译】

      在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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,607评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,047评论 2 379
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,496评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,405评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,400评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,479评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,883评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,535评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,743评论 1 295
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,544评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,612评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,309评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,881评论 3 306
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,891评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,136评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,783评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,316评论 2 342

推荐阅读更多精彩内容

  • 1. Java基础部分 基础部分的顺序:基本语法,类相关的语法,内部类的语法,继承相关的语法,异常的语法,线程的语...
    子非鱼_t_阅读 31,556评论 18 399
  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,579评论 18 139
  • 小编费力收集:给你想要的面试集合 1.C++或Java中的异常处理机制的简单原理和应用。 当JAVA程序违反了JA...
    八爷君阅读 4,565评论 1 114
  • 一. Java基础部分.................................................
    wy_sure阅读 3,784评论 0 11
  • 搞不懂D字开头和G字开头的列车有什么区别,明明内部设施差不多,明明还是很多人,明明还是有塞着耳机托着腮帮,傻傻看向...
    Daisy_vw阅读 304评论 0 1