服務(Services)
從本講開始,所涉及的DDD話題可能與EntityFramework關係不大了。網友千萬別罵我是標題黨,呵呵。由於這部分內容並非是特定於EntityFramework的,更多的是在介紹模式及實踐心得,所以EntityFramework的內容就會偏少了。為了使得針對一些話題的討論能夠延續下去,我仍然將這些文章安排在本系列中,希望讀者朋友能夠諒解。我也在標題中標註了【擴充閱讀】,表示所討論的內容已經不僅僅局限於EntityFramework了。
為了表示補償,透露一下EntityFramework 4.0的最新特性:EF CTP 4.0在“代碼優先”開發模式以及提升開發生產率方面做了重要改進。EF CTP 4.0引入了兩種新的類型:DbContext和DbSet。DbContext是ObjectContext的簡化版。詳情請見http://www.infoq.com/news/2010/07/EF-CTP-4。
言歸正傳,本文將對DDD中的又一重要角色:服務(Services)做一些簡單的介紹。提起服務,很多朋友都會想到“SOA”。而在領域驅動設計裡,服務貫穿於整個系統的各個層面。根據應用系統的領域驅動分層思想,服務被歸類為:應用程式層服務、領網域服務以及基礎結構層服務。應用程式層服務為表現層提供介面,根據DDD的思想,應用程式層很薄,不承擔任何商務邏輯的處理,僅僅是起到coordination的作用。因此,應用程式層服務也不會牽涉到商務邏輯。在CQRS模式中,Command Service以及Query Service就是應用程式層服務。基礎結構層服務是顯而易見的,比如,郵件發送服務、資料服務、事件匯流排等等。這些服務是與領域無關的,只跟技術實現相關。假想這樣一個例子:將貨物從倉庫A轉移到倉庫B,如果轉倉成功,則向倉庫管理員及操作員發送SMS。這是倉儲管理領域常見的業務需求,經典的寫法類似如下:
1: public class TransferService : IDomainService
2: { 3: public void Transfer(Warehouse a,
4: Warehouse b,
5: Item item, Qty qty)
6: { 7: using (IRepositoryTransactionContext ctx = Ioc.GetService<IRepositoryTransactionContext>())
8: { 9: Inventory oItemInA = a.GetInventory(item);
10: if (oItemInA.Qty < qty)
11: { 12: // raise not enough inventory event or exception
13: throw new Exception();
14: }
15: Inventory oItemInB = b.GetInventory(item);
16: if (oItemInB == null)
17: oItemInB = b.CreateInventory(item);
18: oItemInA.Qty -= qty;
19: oItemInB.Qty += qty;
20: ctx.SaveChanges();
21: }
22: }
23: }
24:
在上面的虛擬碼中,我們已經看到了領網域服務(Domain Service)的影子。在DDD裡,領網域服務用以處理那種“放在哪裡都不合適”的商務邏輯。比如上面的轉倉業務,從物件導向的角度看,既不是倉庫應有的操作,也不是貨物(Item)的行為。為了明確領域對象的職責,DDD將這樣的商務邏輯“抽”出來,置於領網域服務當中。對於發送SMS的需求,就需要由應用程式層服務通過“協調”進行處理了。比如:在調用了領網域服務並獲得響應以後,根據響應結果以及外部配置資訊,進而調用基礎結構層服務(SMSService)實現SMS的發送。
看到這裡你會發現,其實哪些業務應該放在實體中,哪些需要使用服務來處理,並沒有一個絕對的標準。還是那句老話:憑經驗。你還會發現,如果從實體將商務邏輯全部“抽”到服務裡,實體將成為僅包含getter/setter的對象,於是貧血模型產生了。正因為DDD提倡面向領域,並將通用語言和領域模型擺在很重要的位置,因此,DDD是不主張貧血模型的。
個人認為,領網域服務的引入,增加了模型的抗需求變更的能力。我們可以通過需求分析,找出商務邏輯中易變的部分,以領網域服務的方式“注入”到領域模型中,今後若有需求變更,則可以無需更改任何現有組件,完成業務處理邏輯的改變。[TBD: 這樣的想法還有待進一步證實]
有關領網域服務的內容,本文暫且討論這些。讀者朋友可以在實踐中提出問題,然後在此與大家分享討論。本文還引出了一個話題,就是應用程式層服務的協調問題。比如,本文的例子中,是在應用程式層服務中調用SMSService實現SMS發送,如果直接將這部分內容寫在應用程式層服務中,勢必降低系統的擴充性。比如,今後希望不僅要發送SMS,而且還要發送Email。DDD的解決方案是引入事件模型。在完成轉倉操作時,向事件匯流排(Event Bus)發送事件,由事件訂閱者Subscriber捕獲並處理(Handle)事件。於是,今後我們只需要實現一個“WarehouseTransferSendEmailEventHandler”的事件處理器,並在Handle Event的調用中,向相關人員發送Email即可。NServiceBus就是一款經典的基於.NET的企業級應用通訊的架構,在基於事件的DDD架構中,NServiceBus發揮了重要作用。
從下一講開始,我將著重討論領域事件以及Event Sourcing,並對DDD的CQRS模式作個引子。