实用模式
工作单元模式和持久化透明
Jeremy Miller
内容
在 Unit 的工作模式
使用工作的单位
持久性不
业务逻辑可以独立于数据库中运行?
可以设计独立地从数据库模型我域模型?
我持久策略在如何影响我的业务逻辑?
多个工作单元
在 4 月 2009 发放的MSDN 杂志 》("持久性模式") 我提供使用某种对象 / 关系时会遇到的一些常见模式映射 (O / RM) 技术来保存业务实体的对象。我认为您或您的团队将在编写您自己 O / RM 工具从头,但这些模式需要知道要有效地使用 (或甚至只是为了选择) 不大可能现有的工具。
本文,我希望继续保持模式的讨论,与工作设计模式的单位,并检查持久性不解决问题。在本文的大部分,我将使用泛型的开票系统为示例问题域。
在 Unit 的工作模式
企业软件开发中常见的设计模式之一是工作单元。根据为 Martin Fowler,工作模式的单位"维护对象受业务交易记录的列表和协调更改和并发性问题的分辨率编写。
工作模式的单位不一定这将显式生成自己,但是模式显示我知道几乎每一个持久性工具中。ITransaction 接口在 NHibernate、 DataContext 类中的,在 LINQ to SQL 和实体框架 ObjectContext 类是工作单元的所有示例。对于该问题,venerable DataSet 可用作的工作单元。
有时,您可以编写您自己特定于应用程序的单位工作接口或类包装内部单位从您的持久性工具的工作。您可能会执行此操作的原因。可以将特定于应用程序的日志记录、 跟踪或错误处理添加到事务管理。您可能希望以封装应用程序的其他工具在持久化的细节。您可能需要此额外的封装,以便于更高版本换出持久性技术。或者您可能希望将 testability 提升您的系统中。许多内置单位从常见的持久性工具的工作实现是难以处理自动测试方案的单位中。
如果您要生成的工作实现您自己单元,它将可能类似此接口:
public interface IUnitOfWork {
void MarkDirty(object entity);
void MarkNew(object entity);
void MarkDeleted(object entity);
void Commit();
void Rollback();
}
您的单位工作的类的需要实体标记为已更改,(新,或已删除的方法。 (在许多实现 MarkDirty 显式调用是工作的不必要因为本身单位有自动确定已更改的实体的某种方式)。 工作单元还有方法来提交或滚备份的所有更改。
方式,您可以认为的工作单元作为转储所有事务处理代码。 工作单元的职责是:
- 管理事务。
- 订单数据库将插入删除,并更新。
- 防止重复的更新。 内部工作对象的单位在一个使用代码的不同部分可能会将相同的发票对象标记为更改,但工作类的单位将仅一个 UPDATE 命令到数据库。
使用的工作模式的单位,值为释放您的代码的其余部分从这些问题,以便可以否则精力集中在业务逻辑。
使用工作的单位
使用工作模式的单位的最佳方式之一是允许不同的类和一个逻辑事务的参与的服务。 此处的要点是希望不同的类和服务保持 ignorant 彼此的同时能够在单个事务中登记。 传统上,您已经能够使用 MTS 类似的事务协调员 / COM+ 或较新的 System.Transactions 命名空间。 个人,我希望允许不相关的类和服务加入逻辑事务中因为我认为它使代码更显式、 易于理解,和简单单元测试使用的工作模式的单位。
假设新开票系统的性能发票生命周期中不同时间独立的操作,在现有发票时。 业务更改这些操作非常频繁并且会经常想要添加或删除新的发票操作,因此让我们应用命令模式 (请参阅" 简化使用命令模式、 MSMQ 和.NET 的分布式的系统设计") 并创建称为 IInvoiceCommand 代表单个独立操作对发票的接口:
public interface IInvoiceCommand {
void Execute(Invoice invoice, IUnitOfWork unitOfWork);
}
IInvoiceCommand 接口有一个简单的 Execute 方法称为执行某种类型的使用发票和 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();
}
}
}
此方法使用的工作单元,可以令人高兴的是混合使用,并匹配的添加或开票的系统中删除业务规则,同时仍能保持事务完整性 IInvoiceCommand 不同实现。
我的经验业务人员似乎关心非常有些延迟、 未付款发票发票使可能要必须生成新的 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 对象相同事务中涉及的测试。 首先,若要测试 IInvoiceCommand 对象的交互的工作单位为,我可能会创建一个假的实现的 IUnitOfWork 严格测试要精确,我会调用在 StubUnitOfWork 录制存根:
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);
}
}
现在,您已获得很好的独立于数据库中运行的工作假冒单元,在 LateInvoiceAlertCommand 的测试装置可能类似中代码 图 2 .
图 2 测试装置 LateInvoiceAlertCommand
[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<AgentAlert>();
}
[Test]
public void the_new_AgentAlert_should_have_XXXXXXXXXXXXX() {
var alert = theUnitOfWork.NewObjects[0].ShouldBeOfType<AgentAlert>();
// verify the actual properties of the new AgentAlert object
// for correctness
}
}
持久性不
选择或为我的项目之一设计一个持久性的解决方案时, 我 mindful 持久性基础结构的影响的对我的业务逻辑代码,至少一个极好的系统中处理的域的业务逻辑。 理想情况下,我希望能够设计、 构建,并测试相对独立于数据库和持久性基础结构代码我业务逻辑。 具体来说,我需要解决方案,支持的持久性不或普通旧 CLR 对象 (POCOs) 理念。
首先,什么是持久性不,以及是否它来自何处? 应用域驱动设计和模式他簿中: 使用 C# 和.NET 中的示例 (Pearson 积矩法教育,Inc.,2006),Jimmy Nilsson 定义 POCOs"…ordinary 类,您关注手头业务问题而不添加基础结构相关的原因的内容。 ... 类应该关注手头业务问题。 没有其他应为域模型中的此类"。
现在,为什么应您关心? 让我们首先讨论要清除持久性不是仅各种设计目标和不的最终目标本身一种手段。 当我将计算一个新的持久性工具时,我通常要求自己以下问题按降序的重要性。 持久性不是完全必需的以满足这些问题,但解决方案通常基于持久性不会更好地比需要业务对象中嵌入的基础结构问题的解决方案。
业务逻辑可以独立于数据库中运行?
这是最重要的问题给我。 我认为所有软件项目的成功最重要的因素之一是能够使用快速反馈循环。 换句话说,我想要缩短时间和"我只需编写新内容"和之间"我被证明新代码正在使我将转到一些其他"或"新代码有问题,因此我将立即修复它"的工作。
在我的职业我始终看到已在其中可以很容易地单元测试的体系结构中的更高效的团队,或甚至只是演练而通过整个应用程序的小段新代码。 相反,我发现体系结构具有一个紧密的业务逻辑和基础结构来将很难进行开发之间运行时间耦合。
让我们 reconsider 在业务规则,指出遇到超过 45 days 打开发票时,您应创建新代理警报的情况。 在更高版本,业务可能您应创建通知在 30 天而是决定。 要验证对发票的警报逻辑,必须首先将代码放到执行对发票和发票旧或比 30 天阈值打开和关闭此规则的状态。 这是体系结构开始重要的点。 可以我向右上到新开票警报逻辑和单元测试一个新的业务规则,或者是否有第一次通过技术圈跳转?
方面的快速的反馈循环最坏的极端是的模型的业务逻辑能不存在基础结构。 是例如第一个 homegrown O / RM 我创建强制您可以编写如下的实体类:
public class Invoice : MySpecialEntityType {
private long _id;
public Invoice(long id) {
_id = id;
// Read in the rest of the Invoice information
// from the database
loadDataFromDatabase(id);
}
public Invoice() {
// Hit the database
_id = fetchNextId();
}
}
此设计,您真正不能创建一个发票类而没有连接到数据库。 在我原始的设计存在是难使用或不正确配置且可用,数据库引擎行使我业务实体类。
当然可以编写自动化的测试对正在访问该的数据库的代码,但它通常在开发人员时间设置比要编写对不依赖于数据库的对象的自动的测试的测试数据要求得更多的努力。 若要设置在数据库中的简单发票,您将不得不采用非空的字段和以满足参照完整性要求无关到业务规则相关的数据。
相反,它将 nicer 只是为了说"是否我有打开发票 31 天,then…"出在代码中,并与之进行? 为此,如何移动多这样的持久性 ignorant 方法的发票类:
// The Invoice class does NOT depend on
// any sort of persistence infrastructure
public class Invoice {
private long _id;
public Invoice() { }
public bool IsOpen { get; set; }
public DateTime? InvoiceDate { get; set; }
}
现在,如果要测试后期开票的预警规则,您可以快速创建打开发票 31 天为如下测试装置中的简单代码测试方案:
[SetUp]
public void SetUp() {
Invoice theLateInvoice = new Invoice() {
InvoiceDate = DateTime.Today.AddDays(-31),
IsOpen = true
};
}
在这种方法,我已分隔后期发票规则和发票类本身不从持久性代码在业务逻辑代码。 只要在内存中您可以轻松创建发票数据,可以快速单元测试业务规则。
作为一个备用,您在选择一个现有的持久性工具时数量应方式实现延迟加载的格外小心。 某些工具将实现使用虚拟的代理服务器模式以隐藏延迟加载有效地到自己的业务实体的透明延迟加载。 其他工具依赖于嵌入直接将业务实体延迟加载的支持,并有效地持久性基础结构从实体创建离合器紧密运行的时间代码生成方法。
几乎与开发人员能够编写自动化的测试时间同样重要,则是与这些测试执行的速度。 自动测试涉及数据访问或 Web 服务访问可以轻松地运行一个数量级或多个测试缓慢的运行完全单个的 AppDomain 中。 这不似乎许多问题,但该项目的大小随时间增加开始,降低运行自动的测试可以轻松地损害团队的生产力,并首先甚至消除自动测试的用途。
可以设计独立地从数据库模型我域模型?
既然我已经在运行时分离数据库中的我的业务层,我所面临的是否我可以设计独立于数据库架构 (反之亦然) 我对象结构问题。 我应该能够设计基于业务逻辑的所需行为我对象模型。 在的数据库而在另一方面,应设计与读取和写入数据以及实施参照完整性的效率。 (顺便,参照完整性是必需的使用 O / RM 工具。
若要说明域模型可以从数据库结构的分化的典型方案,让我们来切换到贸易系统的能源 example 域。 此系统负责跟踪和价格的空间数量销售、 购买,传递。 一个简单的系统在贸易可能包含购买的几个数量和交货的数量的记录。 下面是该的 rub 但: 的度量单位加上的与单位表示的数量。 此贸易的系统让我们假设您跟踪三个不同的单位的数量:
public enum UnitOfMeasure {
Barrels,
Tons,
MetricTonnes
}
如果您要锁定域模型数据库架构的平面结构,您可能会收到这样的类:
public class FlatTradeDetail {
public UnitOfMeasure PurchasedUnitOfMeasure {get;set;}
public double PurchasedAmount {get;set;}
public UnitOfMeasure DeliveredUnitOfMeasure { get; set; }
public double DeliveredAmount { get; set; }
}
使结构处于适合数据库结构和方便的解决方案其中现有数据库中生成代码结构时, 此结构不会不产生自身以执行业务逻辑。
贸易的系统要求要进行比较的数量的能源中业务逻辑的一大部分减去,并添加,但您必须始终假定的度量单位不同,并进行任何比较之前是否介于 1 到另一个转换。 从痛苦经验我状态使用生成直接出的数据库的平面结构的架构使此逻辑 laborious 来实现。
而不是在的平面结构让我们实现货币模式并创建模型中所示的一个的数量的行为的类 图 3 .
图 3 数量
public class Quantity {
private readonly UnitOfMeasure _uom;
private readonly double _amount;
public Quantity(UnitOfMeasure uom, double amount) {
_uom = uom;
_amount = amount;
}
public Quantity ConvertTo(UnitOfMeasure uom) {
// return a new Quantity that represents
// the equivalent amount in the new
// Unit of Measure
}
public Quantity Subtract(Quantity other) {
double newAmount = _amount - other.ConvertTo(_uom).Amount;
return new Quantity(_uom, newAmount);
}
public UnitOfMeasure Uom {
get { return _uom; }
}
public double Amount {
get { return _amount; }
}
}
当您使用该数量类模型,并重复使用周围的度量值的数量单位的常见问题时,TradeDetail 类可能看起来更像这样:
public class TradeDetail {
private Quantity _purchasedQuantity;
private Quantity _deliveredQuantity;
public Quantity Available() {
return _purchasedQuantity.Subtract(_deliveredQuantity);
}
public bool CanDeliver(Quantity requested) {
return Available().IsGreaterThan(requested);
}
}
该数量类将明确使 TradeDetail 逻辑易于实施,但现在,对象模型具有不同从数据库结构。 理想情况下,您持久性工具应支持这种映射。
通常有数据库模型的对象模型之间的差异的问题不会使用简单的业务逻辑的系统上出现。 在这些的系统只是生成从数据库结构的实体类的活动记录体系结构可能是最简单的解决方案。 或者,您可以使用一个允许您从实体对象生成数据库架构的数据库映射工具。
我持久策略在如何影响我的业务逻辑?
并不完美的世界。 任何持久性工具会对形状的实体类的方式的影响的某种。 例如,我的团队在持久性的使用 NHibernate。 由于到 NHibernate 实现延迟加载的属性关系,许多我们属性必须被标记为虚拟只是为了启用延迟加载如发票上此新客户属性:
public class Invoice {
public virtual Customer Customer { get; set; }
}
客户属性被标记为虚拟没有其他原因不要启用 NHibernate 创建动态代理的发票对象提供了一个延迟加载的客户属性
标记成员作为将虚拟严格地为支持延迟加载是的烦恼,但还有更糟糕的是潜在的问题。 递增数团队正在使用驱动设计 (DDD) 的域。 DDD 中常见的策略之一是创建域模型类不能置于无效状态。 域模型类本身也 jealously 将保护其内部数据在内部强制执行业务规则。 要 enact 这一设计理念,一个发票类可能查看更类似于 图 4 。
图 4 域模型发票类
public class Invoice {
private readonly DateTime _invoiceDate;
private readonly Customer _customer;
private bool _isOpen;
// An Invoice must always have an Invoice Date and
// a Customer, so there is NO default constructor
// with no arguments
public Invoice(DateTime invoiceDate, Customer customer) {
_invoiceDate = invoiceDate;
_customer = customer;
}
public void AddDetail(InvoiceDetailMessage detail) {
// determines whether or not, and how, a
// new Invoice Detail should be created
// and added to this Invoice
}
public CloseInvoiceResponse Close(CloseInvoiceRequest request) {
// Invoice itself will determine if the correct
// conditions are met to close itself.
// The _isOpen field can only be set by
// Invoice itself
}
public bool IsOpen {
get {
return _isOpen;
}
}
public DateTime InvoiceDate {
get { return _invoiceDate; }
}
public Customer Customer {
get { return _customer; }
}
}
不包括是精心制作域模型适用于所有类型的应用程序,此方法,但如果请选择该样式的体系结构将会影响您选择的持久性工具。
图 4 中的发票的版本具有实施没有发票没有的发票日期和为客户创建该规则的只有一个构造函数函数。许多,如果不大多数持久性技术要求无参数构造函数。
此外,发票类的此版本 jealously 保护公用 setter 属性没有 _isOpen 相似的内部字段。再次,许多持久性工具可只根据公共属性。如果想要使用更严格的 DDD 样式的实体,您需要将研究是否在持久性工具可以支持将字段映射、 专用属性,或带参数的构造函数函数。
多个工作单元
有与使用的工作模式单位值得考虑一些其他重要问题。如果您感兴趣应用程序中的显式使用的工作模式单位,应是在应用程序的上下文中研究这些问题:
- 在存储库之间的工作单元的职责范围划分。您可能会说所有发生通过储存库读写操作完成的工作单元。也可以使用强制您执行读取和查询通过将状态更改跟踪更容易的工作单位的工作实现的单位。
- 如何将正确单位工作和基本标识图帮助程序传递到各种类执行逻辑事务中的一部分。许多人使用的控件容器的反转正确地将当前的 HttpContext、 线程或其他作用域的策略的工时单位。
持久性不.NET 社区中有些 controversial 并且重要性大量争议。在此时,持久性不和中不支持持久性工具在 Microsoft.NET Framework 本身或扩展.NET 生态系统中可用。NHibernate 是提供最接近,但即使您将不得不危害您的域模型的 NHibernate 启用持久性的某种程度的一个工具。
将您的问题和提出的意见发送至mmpatt@Microsoft.com.
Jeremy MillerC# 的 Microsoft MVP 还是开源的作者StructureMap使用.NET 和在 forthcoming 依赖性注入的工具storyTeller用于 supercharged 适应测试.NET 中的工具。请访问他的博客"带阴影显示树开发人员"CodeBetter 网站的一部分。