[譯文]Unit Of Work
原文鏈結:http://msdn.microsoft.com/en-us/magazine/dd882510.aspx
Unit Of Work是眾多企業級系統設計樣式中較常見的一種。如Martion Fowler所說,Unit Of Work本身即為"用於解決一連串的物件因交易而受到影響,並且將物件狀態的異動寫入之餘處理同步問題"。在建構系統時Unit Of Work樣式並非是必要的,但是可以在許多持久化工具中看到它。NHibernate的ITransaction介面、Linq to SQL的DataContext,以及Entity Framework的ObjectContext皆有實做Unit Of Work。
在某些時候,可能會需要撰寫適合專屬於某應用程式的Unit Of Work介面或從現行使用的持久化工具中提供的Unit Of Work包裝成一個獨立的類別。會需要這麼做很有可能是需要在交易管理中加入應用程式專屬的Log,追蹤,或是錯誤處理。藉由封裝應用程式中指定的持久化工具,即可在往後隨意替換持久化工具。若想要導入測試到系統開發中,許多持久化工具提供的Unit Of Work都無法做到自動化單元測試。
若想要建構專屬於自己的Unit Of Work,其介面會像是如下:
public interface IUnitOfWork{
void MarkDirty(object entity);
void MarkNew(object entity);
void MarkDeleted(object entity);
void Commit();
void Rollback();
}
Unit Of Work會提供用於改變實體物件狀態的方法: new/deleted。(i.e. 在許多實務案例上,取名為MarkDirty並不是必要的,因為Unit Of Work本身擁有許多自動偵測實體是否已經改變的方式。)Unit Of Work亦提供用於Commit或是RollBack的方法。
可以將Unit Of Work想成是一個用來處理交易的類別。Unit Of Work的職責如下:
1. 管理交易。
2. 處理資料庫的新增/刪除/修改。
3. 預防重覆修改。在Unit Of Work物件的內部,不同的程式碼也許會將同一個部件標記成異動狀態,但是Unit Of Work僅只會發出一次Update命令到資料庫。
使用Unit Of Work樣式的價值是解放系統在上述三種情境的處理,而把系統開發的焦點放在商業邏輯上。
使用方式
最佳使用Unit Of Work的方法便是允許不同的類別或服務放到單個的交易邏輯中。這裡的關鍵之處在於要如何讓不同的類別或服務放到單個交易邏輯中。傳統上,可以使用MTS/COM+或是System.Transactions這個命名空間中的類別。於私來說,傾向使用Unit Of Work樣式讓不相關的類別或服務放到單個交易邏輯,因為它可以讓程式碼更易於被讀懂,並且簡化單元測試。
在範例Invoicing系統中,它會在任何間點上對invoce實體進行零散的操作。由於商業邏輯的改變是相當頻繁的, 並且有時會需要額外擴充操作Invoce實體的方法。是故,可以套用命令樣式並且創建一個名為IInvoiceCommand介面,此介面用於離散的Invoce實體操作。
public interface IInvoiceCommand {
void Exec(Invoice invoice, IUnitofWork unitOfWork);
}
IInvoiceCommand介面僅有一個簡單的Exec方法,該方法僅在某種操作Invoice實體的行為發動時被呼叫。任何IInvoiceCommand物件都應該藉由IUnitOfWork介面來持久化任何Invoice實體的異動到資料庫中。
public class InvoiceCommandProcessor
{
private readonly IInvoiceCommand[] m_cmds;
private readonly IUnitOfWorkFactory m_unitOfWorkFactory;
public InvoiceCommandProcessor(IInvoiceCommand[] cmds, IUnitOfWorkFactory unitOfWorkFactory)
{
m_cmds = cmds;
m_unitOfWorkFactory = unitOfWorkFactory;
}
public void runCommands(Invoice invoice)
{
IUnitOfWork unitOfWork = m_unitOfWorkFactory.StartNew();
try{
foreach(IInvoiceCommand cmd in m_cmds) {
cmd.Exec(invoice, unitOfWork);
}
m_unitOfWork.Commit();
}catch(Exception) { m_unitOfWork.Rollback(); }
}
}
使用這個方式就可以快樂地混合並且匹配不同的IInvoiceCommand的實做來任意新增或移除Invoice系統的商業規則之餘還能整合交易功能。
若商業模型中Invoice現在多了延後或是未付費這兩個新需求,那麼就得再創建一個新的IInvoiceCommand類別用於當延遲時要發出警告。
public class LaterInvoiceAlertCommand: IInvoiceCommand{
public void Exec(Invoice invoice, IUnitOfWork unitOfWork) {
bool isLate = isTheInvoiceLate(invoice);
if(!isLate) return;
AgentAlert alert = createLasteAlertFor(invoice);
unitOfWork.MarkNew(alert);
}
}
最佳的解決方案即為LateInvoiceAlertCommand能夠在不同資料庫上開發與測試或是把其它IInvoiceCommand物件放到同一個交易中。首先,測試IInvoiceCommand物件之間在Unit Of Work中的互動,試著去假造一個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);
}
}
}
現在已經有了一個極佳的Unit Of Work的假造(fake)物件,該物件與資料庫無任何相依關係,LateInvoiceAlertCommand的測試程式如下所示:
[TestFixture]
public class When_Creating_an_alert_for_an_invoice_that_is_more_than_45_days_old {
private StudUnitOfWork theUnitOfWork;
private Invoice theLateInvoice;
[SetUp]
public void SetUp() {
//假造的IUnitOfWork物件用來紀錄完成那些工作
theUnitOfWork = new StubUnitOfWork();
//假若已有一個Invoice物件,並且它是45天以前創建的且尚未完工
theLateInvoice = new Invoice { InvoiceDate = DateTime.Today.AddDays(-50), Completed = false};
//實際運行LateInvoiceAlertCommand用來測試Invoice
new LateInvoiceAlertCommand().Execute(theLateInvoice, theUnitOfWork);
}
[Test]
public void the_command_should_create_a_new_AgentAlert_with_the_UnitOfWork(){
//僅是檢驗是否有新的AgentAlert物件被註冊到Unit Of Work
theUnitOfWork.NewObjects[0].ShouldBeOfType<AgentAlert>();
}
[Test]
public void the_new_AgentAlert_should_have_XXXX() {
var alert = theUnitOfWork.NewObjects[0].ShouldBeOfType<AgentAlert>();
//檢驗新AgentAlert物件的屬性是否正確
}
}
省略持久化
當選擇或是設計專案的持久化解決方案時,會特別去關注在商業邏輯中帶有持久化機制所帶來的影響。理想上,設計、建置、測試商業邏輯的關聯性...等等,這些工作的資料庫和持久化程式碼是不相關的。是否有個解決方案能夠提供理想中的持久化省略機制或是POCO?
首先,什麼是省略持久化,並且它是打那來的?在書籍"Applying Domain-Driven Design and Patterns: With Example in C# and .Net"(Pearson Education,Inc., 2006), 作者Jimmy Nilsson定義POCO為: 一群專注在當前手邊的商業問題,並且不含任何與架構相關的東西在裡頭的類別... 這些類別應當專注在當前手邊的商業問題"。商業模型中的類別不該夾雜非商業邏輯外的東西。
現在,我們該注意些什麼?其實省略持久化僅是一個不同的設計方式,而並非是一個必要的設計方式。評估一套持久化工具,通常會問自己下列幾個問題,雖然省略持久化並不能滿足所有的問題,但是省略持久化多半是比僅是為了架構因素就把持久化機制放入商業物件中來得好。
1. 商業邏輯可以在不同資料庫下執行嗎?
這是一個非常重要的問題。一個成功的軟體專案是要能夠快速反饋的。換句話說,欲縮短"我想撰寫一些新東西"以及"我已證明那些新式的程式碼是可運作的,所以我要繼續創新"或"新式程式碼有問題,所以我現在要馬上修改"的時程和所需花的功。
可以觀察到當團隊能夠實行單元測試或僅是實行嘗試在系統中放入一些新式程式碼而不是整個系統,如此便能夠更具生產力。相對的,當系統架構是設計成商業邏輯與架構彼此是緊耦合時,這個系統就會極難開發。
重新思考一下一個新需求:當發現尚未結束的Invoice已超過45天時,需要創建一個新的Agent Alert。在商業邏輯中或許會有將45天改成30天的需求異動的可能性。為了要驗證Invoice在Alert方面的新邏輯,就需要先寫一段程式碼用來紀錄Invoice目前是否已結束以及紀錄是新或是已超過30天。這些就是架構上需要特別考量的部份。是否能夠正確的撰寫並且單元測試新添加邏輯,或著需要先跳脫技術框架?
為了要能夠讓系統快速反饋,最重要的就是有一個不含架構描述的商業邏輯模型。
public class Invoice : MySpecialEntityType{
private long m_id;
public Invoice(long id){
m_id = id;
loadDataFromDatabase(id);
}
public Invoice() {
m_id = fetchNextId();
}
}
使用這種設計方式,就無法建立一個單純的Invoice類別而不去連線到資料庫。若設計成不需要任何與資料庫相關的POCO則就能夠撰寫自動測試並且在測試程式中存取資料庫,並且這個測試程式需要花比撰寫測試程式還要多的開發時間在設定測試資料。為了要簡化設定,需要在資料庫中設定非空值的欄位和資料來滿足需求的整合。
若有那種"我已經超過31天了,然後..."在程式碼中,而且已經有人這麼撰寫了。不妨將Invoice類別搬移到省略持久化的函數中。
public class Invoice{
private long m_id;
public Invoice() {}
public bool IsOpen { get; set;}
public DateTime? InvoiceDate {get; set;}
}
現在,當想要測試落後的Invoice的警告邏輯,可以更快且簡單的建構一個未結束的Invoice並且它已經超過31的測試案例。
[SetUp]
public void SetUp()
{
Invoice theLateInvoice = new Invoice() {
InvoiceDate = DateTime.Today.AddDays(-31),
IsOpen = true
}
}
在上述函數中,已將商業邏輯從Invoice類別的持久化程式碼中分離出來,如此一來,便能夠快速建立Invoice日於記憶體中,可以更快速的對商業邏輯進行單元測試。
當選擇了一套持久化工具後,要注意關於延持載入是否有含括在工具中。某些工具會透過虛擬代理人(Virtual Proxy)樣式來實做更具效率的通透式延遲載入,並且不會在商業邏輯程式碼中出現關於延遲載入的任何邏輯。其它的工具則是相依於程式碼產生技術,並且內嵌可在商業實體物件的延遲載入並且能夠在執行時期做到較有效率的實體物件之間的緊耦合在持久化架構中。
還有一件極重要的事情就是套件作者是否能夠讓開發人員在自動化測試時做到讓單元測試執行速度更快。自動化測試會牽涉到資料存取或是網路服務存取,這會讓測試執行速度變得極慢。這聽起來似乎不是什麼問題,但在專案不斷進行時,執行速度慢的自動測試會降低團隊的生產力且消弭了自動化測試的價值。
2. 是否可以設計出與資料庫模型無關的領域模型
目前已解耦合商業邏輯層與資料庫,接下來就是要設計成與資料庫綱要無關的商業邏輯物件。通常會在商業邏輯物件中加入商業邏輯的行為。資料庫應該是被設計成在讀寫方面都具有高效能之外還具有參考整合性檢驗的能力。
為了要驗證領域模型與資料庫分離的經典案例,先將範例領域切換到能源交易系統。該系統是用於石油買賣/運送的追蹤和付費額度。在系統雛型階段,一個交易可能會包含一定額度的購買以及紀錄運送額度。額度即為評估的單位,並且採用累加來計算。在這個交易系統中,追蹤額度有種單位:
public enum UnitOfMeasure{
Barrels,
Tons,
MetricTonnes
}
倘若領域模型與資料庫綱要綁死,則會如下所示:
public class FlatTradeDetail{
public UnitOfMeasure PurchasesdUnitOfMeasure {get; set;}
public double PurchasedAmount {get; set;}
public UnitOfMeasure DeliveredUnitOfMeasure {get; set;}
public double DeliveredAmount {get; set;}
}
當結構與資料庫結構一致且採用的命名與資料庫也一致,這樣的結構是無法挾帶商業邏輯所需的邏輯。
目前專案要做一個能源交易系統,而系統中一大部份的商業邏輯都是在對額度進行:比較/相減/相加,但總是需要假設評估的單位有可能會進行不同的單位轉換。使用這種直接從資料庫產出的程式碼很難添加商業邏輯到裡面。
創建一個具有額度行為的模型。
public class Quantity{
private readonly UnitOfMeasure m_uom;
private readonly double m_amount;
public UnitOfMeasure Uom {
get { return m_uom; }
}
public double Amount {
get { return m_amount; }
}
public Quantity(UnitOfMeasure uom, double amount){
m_uom = uom;
m_amount = amount;
}
public Quantity ConvertTo(UnitOfMeasure uom)
{
//回傳一個新的Quantity物件, 該物件與Unit Of Measure具有相等數量
}
public Quantity Subtract(Quantity other)
{
double newAmount = m_amount - other.ConvertTo(m_uom).Amount;
return new Quantity(m_uom, newAmount);
}
}
當使用Quantity類別作為模型,並且重用其行為於額度評估單位(Unit of measure quantities)計算,而TradeDetail類別將會是如下所述:
public class TradeDetail {
private Quantity m_urchasedQuantity;
private Quantity m_deliveredQuantity;
public Quantity Available(){
return m_purchasedQuantity.Subtract(m_deliveredQuantity);
}
public bool CanDeliver(Quantity requested) {
return Available().IsGreaterThan(requested);
}
}
Quantity類別會讓TradeDetail的邏輯易於被實做,但是現在的物件模型已經和資料庫綱要大不相同了,理想上,持久化工具應該要支援兩者的物件轉換。
這種物件模型與資料庫模型的差異問題通常都不會有簡單的商業邏輯中。在能源交易系統中,Active Record架構可以簡化實體類別的程式碼產生。或者,可以採用資料庫對應工具來產出資料庫綱要對應的實體物件。
3. 持久化策略對於商業邏輯的影響
現實中,任何持久化工具對於實體類別都會有各種不同的影響。舉個例來說,使用NHibernate作為持久化工具。因為NHibernate實做延遲載入屬性的關聯,而許多屬性必須要被標記為virtual就只是為了能延遲載入,如下所示:
public class Invoice {
public virtual Customer Customer { get; set;}
}
Customer屬性被標記為virtual沒啥原因,就單純是為了讓NHibernate能夠創建一個Invoice的動態代理人,讓Invoice物件能夠作到延遲載入Customer屬性。
強制將屬性標記成virutal來提供延遲載入的功能會帶來一些潛在的問題。在DDD開發中一個常見的方式即為建構領域模型的類別,並且這些類別的屬性值需進行檢查;通常會將屬性存取範圍設定為Internal,這是為了強制將商業邏輯封裝在物件中。
為了達到這個設計上的哲理,Invoice類別修改為:
public class Invoice {
private readonly DateTime m_invoiceDate;
private readonly Customer m_customer;
private bool m_isOpen;
public bool IsOpen {
get { return m_isOpen; }
}
public DateTime InvoiceDate {
get { return m_invoiceDate; }
}
public Customer Customer {
get { return m_customer; }
}
public Invoice(DateTime invoiceDate, Customer customer)
{
m_invoiceDate = invoiceDate;
m_customer = customer;
}
public void AddDetail(InvoiceDatailMessage detail) {
//判斷新的Invoice細節應該在創建後加入到這個Invoice物件中
}
public CloseInvoiceResponse Close(CloseInvoiceRequest request) {
//Invoice自行判斷目前自身的狀態是允許關閉
//m_isOpen欄位僅能由Invoice自行設定
}
}
這種設計的手法會因為系統特質的不同而有不同,但若採用上述的設計方式會影響到持久化工具的選擇。
在最終版本的Invoice類別中僅有一個非預設建構子,並且強迫要實體化Invoice物件就只能將Invoice時間和Customer作為建構子的輸入參數。大多數的持久化工具需要預設建構子。
同樣的,這個版本的Invoice類別對於自己的欄位僅提供getter而沒有setter。再一次提醒,許多持久化工具都是將值設定到setter中。倘若想要使用更多DDD的實體物件設計方式,就必須考慮持久化工具是否可以支援欄位的映射,私有屬性,或是非預設建構子。
更多關於Unit Of Work
還有一些關於Unit Of Work樣式需要考量的問題。若對於怎麼將Unit Of Work應用在專案感興趣,那麼這些議題就需要好好研究。
1. 拆分Repository與Unit Of Work的方式;可將所有和讀取資料放在Repository,而寫入資料方面操作則放在Unit Of Work中。或者為了能夠更方便追蹤實體物件狀態的改變,將所有讀取與寫入操作都在Unit Of Work中實做。
2. 如何將Unit Of Work與其它類別一起放置到一個交易中?很多人都會採用IoC容器來正確地將Unit Of Work放置到HttpContext/執行緒,或是其它範圍性策略。
省略持久化在.Net社群中一直是有爭議的。以目前來說,省略持久化對於大多數的持久化工具來說是不支援的。NHibernate是較佳的持久化選擇,但採用NHibernate就必須考量領域模型的設計要能符合NHibernate。