結合領域驅動設計的SOA分布式軟件架構
引言
本文主要是參考Martion Fowler所著的《企業應用架構模式》與Eric Evans所著的《領域驅動設計》這兩本泰山之作,加上本人在近年實際的工作過程中開發SOA系統所認識到的問題所寫的一篇文章,歡迎各位點評。
最后兩節 細說應用層 、系統總體架構 是本文的重點,著重說明領域驅動設計與SOA之間的關系,對DDD有一定基礎的朋友可以越過前面的幾節,直接查看第七、八節。
源代碼下載 (數據庫可以在.edmx文件根據模型生成)
一、SOA與DDD的定義
SOA與DDD都是常用的系統架構,但兩者之間所針對的核心是不同的。
SOA(面向服務架構)由Gartner在1996年提出來,它是一種分布式的軟件架構,它可以根據需求通過網絡對松散耦合的粗粒度應用組件進行部署、組合和使用。簡單來說,SOA就是一種大型系統開發的體系架構,在基于SOA架構的系統中,具體應用程序的功能是由一些松耦合并且具有統一接口的組件(也就是service)組合構建起來的,它是針對多核心多平臺之間的數據交換。
DDD(領域驅動設計)由Eric Evans在2004提出,它的核心內容是“如何將業務領域概念映射到軟件工程當中”。它推翻了“軟件從數據層開發設計”的舊習慣,強調領域模型在軟件中發揮的強大力量,注重如何把企業內部復雜的業務流程轉化為軟件。
也許可以認為SOA針對的是大型系統的總體架構,注重如何把系統進行項目分離,隔離開發,最后實現系統合并。而DDD是針對單個項目的開發管理過程,注重如何利用領域模型把業務需求轉化為軟件。兩者之間并沒有存在理論上的沖突,能把兩者結合,各展所長,更能發揮各自的優勢。
二、DDD的分層結構
1. 概念
從概念上來說,領域驅動設計架構主要分為基礎設施層、領域層、應用層、表現層4個概念層。
基礎結構層:是為各層提供各項通用技術能力而構建的,它可以為領域層提供像Hibernate、LINQ、ADO.NET等持久化機制,為應用層傳遞消息,為表現層提供插件等等。
領域層:它是系統的核心部分,代表業務的邏輯概念。它會根據業務的實際流程定制了業務信息以及業務規則,并按一定的關系制定領域模型。領域模型盡管需要依賴基礎結構層進行保存,但領域模型之間的邏輯關系是跟基礎結構層相隔離的。即使基礎結構層從NHibernate技術轉換成LINQ技術,也不會影響到領域層的結構。領域模型只會依賴實際的業務邏輯,它只會根據業務的轉變而靈活變動。
應用層:它的任務是協調領域層與表現層之間的關系,也可以作為系統與外界溝通的橋梁,在這層里面不會包括任何的業務邏輯。在SOA面向服務架構,這一層起著重要的作用,在第七節將詳細說明。
表現層:它是常用的界面開發,可以以頁面(ASP.NET、JSP),窗口(WinForm、WPF、Swing)等形式表現,它的主要職責是負責與用戶進行信息溝通。(注意:在一般的項目開發中,Web服務會作為與外界通訊的接口放置在表現層中,但在SOA中,Web服務會大多置于應用層中,下面將會作進一步解釋)
2. 開發實例
在此先舉個常見的訂單管理例子,在下面的章節里都會以這個實例為參考:
每個用戶在Person表里面都會有一個對應的帳戶,里面記錄了用戶的姓名、地址、電話、積分(Point)等基本信息。
在Order表里記錄的是每次交易的過程,每次商品的送貨費(Freightage)為10元,當商品價格(Price)超過98元可免費送貨,當用戶Person積分(Point)超過2000分可獲7折優惠(Favorable),1000~2000分可獲8折,1000分以下可有9折,最后總體價格為為(TotalPrice)。
在最后結單的時候Order表里會產生訂單號碼OrderNumber和下訂日期Delivery,Person表的積分也會加上訂單總價的點數。
最后OrderItem表包含了物品Goods、物品價格Price、購買數量Count等屬性,它主要記錄每次訂單的詳細交易狀況。
上面的業務邏輯跟淘寶、當當等等大型購物網基本相似。之所以用這樣一個例子作為參考,是想表現一下DDD是如果利用領域模型去適應多變的業務邏輯關系。
三、把業務關系轉化為領域模型
1. 概念
模型驅動設計(MODEL-DRIVEN-DESIGN)是DDD里面的核心,它代表的是各個對象之間的關系,把復雜的邏輯關系轉化為模型。
模型主要分為實體(Entity)、值對象(Value Object)與服務(Service)三種。
實體:實體所包含的不單止是一連串的屬性,更重要的是與事件的聯系,在一個生命周期中環境的變化與事件發生,將引起實體內部產生變化。好像在實體Order里面,Person的積分(Point)和OrderItem的價格(Price)都會直接影響總體價格(TotalPrice)的大小,而總體價格也會影響到運費Freightage的多少等等。在Order實體的一切,都會受到Person、OrderItem等這些外部因數的影響,這樣的對象被視為實體。在不同的時刻,實體會有不同的狀態,所以在開發過程中我們需要為實體加上一個“標識符”來區分對象的身份,它是實體的生命周期里的唯一標志。
值對象:當所用到的對象只有屬性而沒有其他邏輯關系的時候,我們就可以把它視為是值對象。值對象沒有狀態,也不需要有 “標識符”。在多數情況下它可以作為一個屬性存在于一個實體的內部。一般情況下值對象的屬性是不可改變的,當需要更改屬性時,可以把整個對象刪除,然后重新加入一個新對象。
服務:當實體之間存在某些操作,它們并不單一地附屬于某一個實體,而是跟多個實體都有關聯的時候,就可以使用服務來封裝這些操作。值得注意的是服務并非單獨指Web Service, 也并非單單存在于領域層,而是在各個層當中都會存在服務,每一層的服務都有著不同的職能。在基礎結構層服務可能是用于構建身份驗證、電子郵件、錯誤處理等等操作;在領域層,服務更多時候是一種操作,它用于協調多個實體之間的關系,處理各類的業務問題;在應用層(特別是在分布式開發系統內),服務多以Web Service、TCP/IP套接字、MSMQ等等方式實現,服務在此處會作為一個與外界通訊的接口。
- 備注 :這里面也存在一定的爭義,Eric 認為實體所代表的只是多個對象之間的關系,而它們的動作將會由服務來體現出來,這被稱為貧血型模型。但在開發過程中,越來越多人會把動作加入到實體里面,這被稱為充血型模型。其實不同的問題應該客觀分析,分別對待,在這個例子里面將會以按照 Eric 的定義來開發服務,在后面的開發過程中大家也可以從中體現一下服務層所帶來的好處。
2. 實例說明
先以ADO.NET Entity Framework實現模型,Person、Order分別屬于兩個實體,它們都將繼承Root接口,在它們的生命周期內都會生成一個Guid作為標志。此處把OrderItem作為一個值對象置于Order實體內,這意味著OrderItem會通過Order來獲取,外界不能跨越Order直接獲取OrderItem。當然這應該由具體的業務情況來確定,當外界需要單獨調用OrderItem類的時候,就應該考慮把OrderItem獨立成為一個實體類。
在這里可利用分部類為實體增加Guid屬性,關于分部類于分部方法的詳細介紹可參考C#綜合揭秘——分部類和分部方法
namespace Business.DomainModel { public interface Root { } public partial class Order:Root { private Guid _guid; public Order() { _guid = System.Guid.NewGuid(); } //為根對象設置唯一的Guid; public Guid GUID { get { return _guid; } } } public partial class Person:Root { public Person() { _guid = System.Guid.NewGuid(); } //為根對象設置唯一的Guid; private Guid _guid; public Guid GUID { get { return _guid; } } } }
四、細說Repository
1. 概念
Repository是把持久化對象轉換成領域模型的一種方式,可用于獲取、更新持久對象并管理它們的生命周期。它使應用程序與持久化技術實現解耦,程序無需受制于使用Oracle還是MySql數據庫,也不會受到Hibernate、LINQ、ADO.NET等數據層的約束,使開發人員可以把注意力集中到領域模型當中。
Repository與傳統三層模式的DAL層有點相似,但Repository針對的是每一個根對象來劃分邊界的。在這個例子當中,Person與Order都會有對應的PersonRepository、OrderRepository。而OrderItem只是Order的子屬性,所以它的插入、更新、刪除都會包含在OrderRepository當中。當多個對象之間建立起聯系后,關系將是復雜的,特別是在LINQ里面,程序可以輕易通過Person的導航屬性里獲取OrderItem的值,最后很容易使代碼變得混亂。所以確立Repository的邊界,可以在有效管理每個Repository的職能。
2. 實例說明
注意OrderItem的存取、刪除都包含在OrderRepository里面。在獲取、修改Order的時候,也會利用“顯式加載” context.Order.Include("OrderItem") 的方法,使OrderItem實現同步更新。而通過PersonRepository.GetPerson(int)獲取的Person對象,它內部的Order屬性將是null值,這必須清晰按照領域模型的邊界劃分的。
當LINQ面世以后,數據的獲取變得簡單,特別在一些小型的系統開發時,很多人會不自覺地把這種領域模型的分界規則打破。但隨著系統的復雜化,問題就會逐漸呈現。比如當Order對象的屬性被更新,使用OrderRepository.Update(Order)更新數據庫后,頁面的Person對象未能同步實現更新,在Person與數據庫交換數據的時候,Order又被變回舊值。
在混亂的數據層開發中,這種情況非常常見,所以在下會堅持Repository的原則,把Repository的職能清晰按照領域模型劃分。
namespace Business.IRepository { public interface IOrderRepository { Order GetOrder(int id); IList<Order> GetList(); IList<Order> GetListByPerson(int personID); int AddOrder(Order order); int DeleteOrder(int id); int UpdateOrder(Order order); int AddOrderItem(OrderItem orderItem); int DeleteOrderItem(int id); } public interface IPersonRepository { int AddPerson(Person person); int AttachPerson(Person person); int UpdatePerson(Person person); Person GetPerson(int id); IList<Person> GetList(); } } namespace Business.Repository { public class OrderRepository:IOrderRepository { //根據ID獲取單個Order public Order GetOrder(int id) { BusinessContext _context = new BusinessContext(); Order order = null; try { using (TransactionScope scope = new TransactionScope()) { //由于OrderItem是Order實體中的一個屬性,必須通過OrderRepository同步獲取 var list = _context.Order.Include("OrderItem") .Where(x => x.ID == id); if (list.Count() > 0) order = list.First(); else order = new Order(); scope.Complete(); } } catch (Exception ex) { //出錯處理,并返回一個空對象 Business.Common.ExceptionManager.DataException.DealWith(ex); order = new Order(); } _context.Dispose(); return order; } .................. .................. } public class PersonRepository:IPersonRepository { public int AddPerson(Person person) { return LinqHelp.Add<Person>(person); } public Person GetPerson(int id) { return LinqHelp.Get<Person>(id); } ................. ................. } }
在更新Order這種復雜的領域模型時,如果要分辨單個OrderItem屬性是新建值還是更新值,然后分別處理,那將是比較麻煩的,而且OrderItem只是一個值對象,ID編碼等屬性對它沒有任何實在意義。所以在更新List<OrderItem>屬性時都會先把它全部刪除,然后重新加載,在OrderItem數量不多的時候,這是一種十分有效的方法。
namespace Business.Repository { public class OrderRepository:IOrderRepository { ................. ................. //更新Order,因為難以別哪些是原有的OrderItem,哪些OrderItem是新插入 //使用簡單的方法,會先把原有的OrderItem的刪除,再重新插入 public int UpdateOrder(Order order) { int returnValue = -1; BusinessContext _context = new BusinessContext(); try { using (TransactionScope scope = new TransactionScope()) { var list = _context.Order.Include("OrderItem") .Where(x => x.ID == order.ID); if (list.Count() > 0) { //更新Order列 Order _order = list.First(); _order.Count = order.Count; _order.Delivery = order.Delivery; _order.Favorable = order.Favorable; _order.Freightage = order.Freightage; _order.OrderNumber = order.OrderNumber; _order.PersonID = order.PersonID; _order.Price = order.Price; _order.TotalPrice = order.TotalPrice; //刪除原有的訂單明細項OrderItem if (list.First().OrderItem.Count != 0) foreach (var item in list.First().OrderItem) DeleteOrderItem(item.ID); //加入新的訂單明細項OrderItem if (order.OrderItem.Count != 0) { foreach (var item in order.OrderItem) { var _orderItem = new OrderItem(); _orderItem.Count = item.Count; _orderItem.Goods = item.Goods; _orderItem.OrderID = item.OrderID; _orderItem.Price = item.Price; AddOrderItem(_orderItem); } } returnValue = _context.SaveChanges(); } else returnValue = 0; scope.Complete(); } } catch (Exception ex) { Business.Common.ExceptionManager.DataException.DealWith(ex); returnValue=-1; } _context.Dispose(); return returnValue; } //插入OrderItem public int AddOrderItem(OrderItem orderItem) { return LinqHelp.Add<OrderItem>(orderItem); } //刪除OrderItem public int DeleteOrderItem(int id) { EntityKey key = new EntityKey("BusinessContext.OrderItem", "ID", id); return LinqHelp.Delete(key); } } }
五、領域層的服務
1. 例子說明
在第二節已基本介紹過服務的作用了,領域層服務的作用主要是為了解決業務上的邏輯問題,更多的時候,服務是一個與業務相關的動作。比如在上述例子中:
在Order表里記錄的是每次交易的過程,每次商品的送貨費(Freightage)為10元,當商品價格(Price)超過98元可免費送貨,當用戶 Person積分(Point)超過2000分可獲7折優惠(Favorable),1000~2000分可獲8折,1000分以下可有9折,最后總體價 格為為(TotalPrice)。
這復雜的業務邏輯,完全可以由一個領域服務類AccountManager來完成
namespace Business.Service.DomainService { public class AccountManager { private Person _person; private Order _order; public AccountManager(Person person, Order order) { _person = person; _order = order; } ///計算總體收費 public void Account() { //計算商品數量 GoodsCount(); //計算商品價格 PriceAccount(); //計算優惠等級 FavorableAccount(); double price1 = (_order.Price - _order.Favorable).Value; //計算運費 FreightageAccount(price1); //計算總體價費 _order.TotalPrice = price1 + _order.Freightage.Value; } //計算商品數量 private void GoodsCount() { _order.Count=0; foreach (var OrderItem in _order.OrderItem) _order.Count += OrderItem.Count; } //商品總體價格 private void PriceAccount() { _order.Price = 0; foreach (var OrderItem in _order.OrderItem) _order.Price += OrderItem.Price * OrderItem.Count; } //優惠分為三等,積分小于1000有9折,小于2000分為8折,大于2000為7折 private void FavorableAccount() { int point = (int)_person.Point.GetInt(); if (point < 1000) _order.Favorable = _order.Price * 0.1; if (point >= 1000 && point < 2000) _order.Favorable = _order.Price * 0.2; if (point > 2000) _order.Favorable = _order.Price * 0.3; } //如果價格在98元以上,可免運費。其余運費為10元 private void FreightageAccount(double price) { if (price >= 98) _order.Freightage = 0; else _order.Freightage = 10; } } }
你可能會說,在這個業務流程中,除了積分優惠Person.Point以外,其他的業務都只與Order的屬性有關,按照充血型模型的方案,完全可以把這些業務放到Order的方法當中,而把積分優惠獨立成為一個服務。但在下在很多的開發過程中發現,為模型附上動作會帶來一連串的問題,好像你不知道哪些操作應該在模型動作上實現,哪里應該在服務中實現......。對于這些無休止的爭論不會因為這里的一個小例子而停止,但在這里我會堅持使用貧血型模型,利用服務來完成所有的動作。
再舉一個例子:在最后結單的時候Order表里會產生訂單號碼OrderNumber和下訂日期Delivery,Person表的積分也會加上訂單總價的點數。對應這個操作,也可以單獨開發一個PaymentManager服務類進行管理。
namespace Business.Service.DomainService { public class PaymentManager { //下單結算 public void Payment(Order order) { //確定下單,建立訂單號 OrderManager orderManager = new OrderManager(); order.OrderNumber = Guid.NewGuid().ToString(); order.Delivery = DateTime.Now; orderManager.UpdateOrder(order); //為用戶增加會員分數 PersonManager personManager = new PersonManager(); personManager.AddPoint(order.PersonID, (int)order.TotalPrice); } } }
利用領域層的服務,使得每個Manager服務類的職能非常明確,業務管理起來也十分地方便,領域層可以隨著業務的改變而靈活變動。最后加上實體基本的插入、更新、獲取等等操作,領域層的服務就可以清晰地完成。
可以注意到每次加入新Order,或者更新Order的時候,都會調用到AccountManager對象來重新計算費用
namespace Business.Service.DomainService { public class OrderManager { private IOrderRepository orderRepository = DataAccess.CreateOrderRepository(); public int AddOrder(Order order) { //計算Order總體費用 Account(order); //加入修改后的Order return orderRepository.AddOrder(order); } public int UpdateOrder(Order order) { Account(order); return orderRepository.UpdateOrder(order); } public int AddOrderItem(OrderItem orderItem) { int index = orderRepository.AddOrderItem(orderItem); Order order = orderRepository.GetOrder(orderItem.OrderID); UpdateOrder(order); return index; } public int DeleteOrderItem(OrderItem orderItem) { int index = orderRepository.DeleteOrderItem(orderItem.ID); Order order = orderRepository.GetOrder(orderItem.OrderID); UpdateOrder(order); return index; } //計算Order總體費用 private void Account(Order order) { //獲取對應Person對象 IPersonRepository personRepository = DataAccess.CreatePersonRepository(); Person person = personRepository.GetPerson(order.PersonID); //調用服務層的AccountManager對象,計算費用,修改Order AccountManager accountManager = new AccountManager(person, order); accountManager.Account(); } public int DeleteOrder(Order order) { return orderRepository.DeleteOrder(order.ID); } .................. .................. } public class PersonManager { private IPersonRepository personRepository = DataAccess.CreatePersonRepository(); public int AddPerson(Person person) { return personRepository.AddPerson(person); } //增加分數 public int AddPoint(int personID,int point) { Person person = personRepository.GetPerson(personID); person.Point=person.Point.GetInt()+point; return personRepository.UpdatePerson(person); } ............... ............... } }
六、工廠模式Factory
Factory是常用到軟件開發模式,在網上像簡單工廠、工廠方法、抽象工廠等開發模式的資料都到處可尋,可這并不是領域驅動設計的主題。在這一節里,我主要想介紹Factory的適用時機。
并非生成所有對象的時候,都需要用到工廠模式。在生成簡單對象的時候,可以直接利用構造函數來代替工廠,也可以添加工廠方法來生成對象。但如果在生成對象時,內部屬性之間存在一系統復雜的業務規則的時候,就可以把生成方法獨立到一個Factory類里面。這時候客戶端無需理會潛在邏輯關系,而直接通過這個Factory來生成相應的對象。
舉個例子,在新建Order的時候,業務上規定運費是總體金額的1%,折扣規定是7.5折...... 。如果由客戶端新建一個對象Order,然后為這些屬性賦值,那相關的業務邏輯就會暴露在外。這時候就可以使用Factory模式,把屬性之間的關系封裝到Factory之內,客戶端通過Factory就能輕松地生成Order對象而無需要理會復雜的內部關系。
至于較復雜的Factory模式,在此不多作介紹,各位可以在網上查找相關資料。
七、細說應用層
1. SOA系統中應用層的特點
在開發SOA分布式系統的時候,應用層是一個重點,它是一個運輸中心和信息發放的端口,擔負著數據轉換與數據收發的責任。它有以下的特點:
- 單一性
應用層只是一個接駁信息的橋梁,當中不應該包含任何業務邏輯的成分,任何與業務有關元素都應該封裝在領域層。
- 粗粒度
分布式系統與普通網站和應用程序不同,因為它假定外界對系統內部是毫無了解的,用戶只想輸入相關數據,最后得到一系列計算結果。所以我們應該把計算結果封裝在一個數據傳輸對象(DTO)內,實現粗粒度的傳遞,這是一般項目與SOA系統在服務層的一個最明顯的差別。 想想如果一個頁面需要同時顯示一個顧客的個人資料、某張訂單的詳細資料,那將要同時獲取Person、Order、OrderItem三張表的信息。在普通系統的開發過程中,這并不會造成太大問題,但在使用遠程服務的時候,如果用三個方法分別獲取,那將會造成不少的性能損耗。特別是在分布式開發系統中,應用層與表現層之間是實現分離的,更多時候兩者是由不同部門所開發的模塊,表現層不會了解應用層中的邏輯關系,Person,Order,OrderItem三樣東西在表現層看來,也就是同一樣東西,那就是返回值。所以在系統內,應該把多張表的信息封裝在一個DTO對象內,通過應用層一個遠程方法一次性返還。使用粗粒度的數據元素是分布式系統的一個特點。
- 傳輸性
如果你熟悉SOA系統,對DTO(Data Transfer Object 數據傳輸對象)這個詞一定并不陌生。DTO屬于一個數據傳輸的載體,內部并不存在任何業務邏輯,通過DTO可以把內部的領域對象與外界隔離。DTO所封裝的是客戶端的數據,所以它的設計更多地是針對客戶端的需求,而不是業務邏輯。比如說本來Person與Order是一對多的關系,但當一個頁面只要顯示的是一個客戶的單張訂單信息,那我們就可以根據需要把DTO中的Person和Order設計為一對一的關系。如果你是使用MVC開發一般的網站,更多時候會把返回對象直接轉化為Model。如果你開發是一個分布式系統,那更多時候會從系統性能與隱藏業務邏輯出發著想。而且考慮到把內部對象轉化為DTO,將是一件麻煩的事,建議應該考慮DTO的兼容性,使DTO可以作為多個方法的返還載體。(注意:在SOA系統內,應該從性能出發優先考慮粗粒度元素的傳輸性問題)
- 封裝性
應用層并不需要復雜的模型,只需把一些功能封裝在少數的幾個應用層服務類里面,使用Web Service、TCP/IP套接字、MSMQ等服務方式向外界發布。
說到這里,我真的十分感激Martin先生帶給我的幫助,在開發過程中,這些復雜的問題帶給我不少的困擾,Martin先生一紙富有經驗的獨特見解,真的帶給在下很大的啟發。
2. 應用層的協調性
應用層服務會利用Repository,完成實體基本的插入、更新、獲取等等操作,并調用領域層的服務管理的業務邏輯。注意觀察,一切的業務邏輯都只會隱藏于領域層,應用層服務只起著協調作用,本身不應該包含有任何業務邏輯。
可以看到OrderService就是通過調用AccountManager、PaymentManager等領域層服務來完成結賬、付款等一系列復雜業務邏輯的。
namespace Business.Service.ApplicationService { public class PersonService { private IPersonRepository personRepository = DataAccess.CreatePersonRepository(); public int AddPerson(Person person) { return personRepository.AddPerson(person); } public int UpdatePerson(Person person) { return personRepository.UpdatePerson(person); } public Person GetPerson(int personID) { return personRepository.GetPerson(personID); } public IList<Person> GetList() { return personRepository.GetList(); } } public class OrderService { private IOrderRepository orderRepository = DataAccess.CreateOrderRepository(); public int AddOrder(Order order) { //計算Order總體費用 Account(order); //加入修改后的Order return orderRepository.AddOrder(order); } //調用領域層服務AccountManager,計算Order總體費用 private void Account(Order order) { //獲取對應Person對象 IPersonRepository personRepository = DataAccess.CreatePersonRepository(); Person person = personRepository.GetPerson(order.PersonID); //調用服務層的AccountManager對象,計算費用,修改Order AccountManager accountManager = new AccountManager(person, order); accountManager.Account(); } //調用領域層服務PaymentManager,確認訂單 public Order Payment(int orderID) { var order=orderRepository.GetOrder(orderID); if (order != null) { PersonRepository personRepository = new PersonRepository(); var person=personRepository.GetPerson(order.PersonID); PaymentManager paymentManager = new PaymentManager(); paymentManager.Payment(order, person); orderRepository.UpdateOrder(order); personRepository.UpdatePerson(person); return order; } else throw new Exception("Can not find order!"); } public int DeleteOrder(Order order) { return orderRepository.DeleteOrder(order.ID); } public Order GetOrder(int orderID) { return orderRepository.GetOrder(orderID); } public IList<Order> GetList() { return orderRepository.GetList(); } public IList<Order> GetListByPerson(int personID) { return orderRepository.GetListByPerson(personID); } public int UpdateOrder(Order order) { Account(order); return orderRepository.UpdateOrder(order); } public int AddOrderItem(OrderItem orderItem) { int index = orderRepository.AddOrderItem(orderItem); Order order = orderRepository.GetOrder(orderItem.OrderID); UpdateOrder(order); return index; } public int DeleteOrderItem(OrderItem orderItem) { int index = orderRepository.DeleteOrderItem(orderItem.ID); Order order = orderRepository.GetOrder(orderItem.OrderID); UpdateOrder(order); return index; } ....................... ....................... } }
3. 數據轉換過程
實現領域對象與DTO之間的轉換是一件復雜的事件,因此可以建立一個數據轉換器實現此功能。
在平常的工作里,不太多會把“訂單管理系統”做成SOA的模式,因為在分布式系統中,數據的格式與定義大多數由部門之間協定,其中包含明確的規則。但由于條件的局限,在這里還是想以訂單管理為例子,希望可以帶給你一定的幫助。在購物車結賬,頁面會包含用戶基本信息,當前訂單信息,訂單明細信息等多個部分。
首先可以根據頁面建立DTO對象,在分布式系統中,通常會把DTO對象放在一個獨立的命名空間里,在這個實例里面稱之為Business.TransferObject。DTO對象更多時候是面向表現層的需求而建立,這里由于表現層頁面所需要的只是單個用戶,單張訂單的數據,所以在OrderDTO對象里會包含了用戶信息和訂單資料,也存在訂單詳細列List<OrderItemDTO>。當然,DTO的設計可以隨著需求而修改。
在SOA系統里,DTO是遠程服務數據的載體,所以會把DTO附上可序列化特性,這此例子中會使用WCF的數據契約實現OrderDTO和OrderItemDTO。
這里OperationAssembler就是一個數據轉換器,它是數據轉換的核心,它是領域對象與DTO之間實現轉換的工具。要在多個對象之間實現數據轉換實在是一件非常麻煩的事,所以我一直提倡注意DTO對象的兼容性,使單個DTO對象可以適用于多個外觀層,以減少數據轉換所帶來的麻煩。
namespace Business.Service.ApplicationService { public class OperationAssembler { //把領域對象轉換成DTO public static OrderDTO GetOrderDTO(Order order,Person person) { OrderDTO orderDTO = new OrderDTO(); if (person != null) { orderDTO.EMail = person.EMail.GetString(); orderDTO.Address = person.Address.GetString(); orderDTO.Name = person.Name.GetString(); orderDTO.PersonID = person.ID; orderDTO.Point = person.Point.GetInt(); orderDTO.Telephone = person.Telephone.GetString(); } if (order != null) { orderDTO.PersonID = order.PersonID; orderDTO.Count = order.Count.GetInt(); orderDTO.Delivery = order.Delivery.GetDateTime(); orderDTO.Favorable = order.Favorable.GetDouble(); orderDTO.Freightage = order.Freightage.GetDouble(); orderDTO.OrderID = order.ID; orderDTO.OrderNumber = order.OrderNumber.GetString(); orderDTO.Price = order.Price.GetDouble(); orderDTO.TotalPrice = order.TotalPrice.GetDouble(); var orderItemList = order.OrderItem.ToList(); if (orderItemList.Count != 0) { var orderItemDTO = new List<OrderItemDTO>(); foreach (var orderItem in orderItemList) orderItemDTO.Add(GetOrderItemDTO(orderItem)); orderDTO.OrderItemList = orderItemDTO; } } return orderDTO; } public static OrderItemDTO GetOrderItemDTO(OrderItem orderItem) { OrderItemDTO orderItemDTO = new OrderItemDTO(); orderItemDTO.Count = orderItem.Count.GetInt(); orderItemDTO.Goods = orderItem.Goods.GetString(); orderItemDTO.OrderID = orderItem.OrderID; orderItemDTO.OrderItemID = orderItem.ID; orderItemDTO.Price = orderItem.Price.GetDouble(); return orderItemDTO; } //把DTO轉換成多個對象 public static void SetOrder(OrderDTO orderDTO, out Person person, out Order order) { person = new Person(); person.EntityKey=new System.Data.EntityKey("BusinessContext.Person","ID",orderDTO.PersonID); person.Address = orderDTO.Address; person.EMail = orderDTO.EMail; person.ID = orderDTO.PersonID; person.Name = orderDTO.Name; person.Point = orderDTO.Point; person.Telephone = orderDTO.Telephone; order = new Order(); order.EntityKey=new System.Data.EntityKey("BusinessContext.Order","ID",orderDTO.OrderID); order.Count = orderDTO.Count; if (orderDTO.Delivery.Year!=0001&&orderDTO.Delivery.Year!=9999) order.Delivery = orderDTO.Delivery; order.Favorable = orderDTO.Favorable; order.Freightage = orderDTO.Freightage; order.ID = orderDTO.OrderID; order.OrderNumber = orderDTO.OrderNumber; order.PersonID = orderDTO.PersonID; order.Price = orderDTO.Price; order.TotalPrice = orderDTO.TotalPrice; var orderItemDTOList = orderDTO.OrderItemList; if (orderItemDTOList.Count() != 0) foreach (var orderItemDTO in orderItemDTOList) order.OrderItem.Add(GetOrderItem(orderItemDTO)); } public static OrderItem GetOrderItem(OrderItemDTO orderItemDTO) { OrderItem orderItem = new OrderItem(); orderItem.EntityKey = new System.Data.EntityKey("BusinessContext.OrderItem", "ID", orderItemDTO.OrderItemID); orderItem.Count = orderItemDTO.Count; orderItem.Goods = orderItemDTO.Goods; orderItem.ID = orderItemDTO.OrderItemID; orderItem.OrderID = orderItemDTO.OrderID; orderItem.Price = orderItemDTO.Price; return orderItem; } } } //數據傳輸對象 DTO namespace Business.TransferObject { [DataContract] public class OrderItemDTO { private int _orderItemID; private int _orderID; private string _goods; private double _price; private int _count; [DataMember] public int OrderItemID { get { return _orderItemID; } set { _orderItemID = value; } } ............ ............ } [DataContract] public class OrderDTO { private int _personID; private string _name; private string _address; private string _telephone; private int _point; private string _email; private int _orderID; private string _orderNumber; private int _count; private double _freightage; private double _favorable; private DateTime _delivery; private double _price; private double _totalPrice; private IList<OrderItemDTO> _orderItemDTOList; [DataMember] public int PersonID { get{return this._personID;} set{this._personID=value;} } .......... .......... } }
4. 應用層的發布
在開發SOA系統的時候,應用層的服務需要使用遠程方法對外開放,在接收到請求的時候,它可以調用領域層服務獲取運算結果,然后通過數據轉換器OperationAssembler把運算結果轉換成DTO,最后返還到表現層。在起初,我曾嘗試對應每個應用層的對象建立一個遠程接口,但經過多次重構以后,我覺得行程對象就是一個簡單的對外接口,對象之間不存在什么邏輯關系。所以更簡單的方法是使用外觀模式,建立少數的幾個遠程服務類,把所有的應用層對象的方法都包含在內。
可以留意代碼,OperationService包括了對Person模型和Order模型的所有操作。而且每個操作都只是簡單地調用領域層服務 (DomainService) 獲得計算結果,然后使用數據轉換器 (OperationAssembler)轉換數據,當中并不存在任何的業務邏輯。
namespace Business.Service.ApplicationService { [ServiceContract] public interface IOperationService { [OperationContract] int AddOrder(ref OrderDTO orderDTO); [OperationContract] int DeleteOrder(OrderDTO orderDTO); [OperationContract] int UpdateOrder(ref OrderDTO orderDTO); [OperationContract] IList<OrderDTO> GetOrderByPerson(int personID); [OperationContract] OrderDTO GetOrder(int orderID); [OperationContract] int AddPerson(ref OrderDTO orderDTO); [OperationContract] int UpdatePerson(ref OrderDTO orderDTO); [OperationContract] OrderDTO GetPerson(int personID); [OperationContract] IList<OrderDTO> GetPersonList(); } } namespace Business.Service.ApplicationService { public class OperationService:IOperationService { [OperationBehavior(TransactionScopeRequired = true, TransactionAutoComplete = true)] public int AddOrder(ref OrderDTO orderDTO) { OrderManager orderManager = new OrderManager(); Order order = GetOrder(orderDTO); int n = orderManager.AddOrder(order); orderDTO = OperationAssembler.GetOrderDTO(order, null); return n; } [OperationBehavior(TransactionScopeRequired = true, TransactionAutoComplete = true)] public int DeleteOrder(OrderDTO orderDTO) { OrderManager orderManager = new OrderManager(); return orderManager.DeleteOrder(GetOrder(orderDTO)); } .................... .................... } }
八、系統總體架構
1. 體現領域驅動設計的架構
到此總結一下領域驅動設計DDD的總體結構,Repository先使用ORM映射或SQL命令等方式把持久化數據轉化為領域對象,然后根據業務邏輯把動作轉化為領域層的服務(Domain Service),操作過程中所針對的都是領域對象Domain Object 。接著,應用層通過數據轉換器把領域對象Domain Object轉化為數據傳輸對象DTO,再利用應用層的服務(Application Service)開通對外的接口,最后表現層通過遠程調用獲取數據。
注意留意的是SOA系統中,UI表現層與Application Service應用層服務是實現分離的,表現層可以同時調用多方的遠程服務來完成工作。
2. 體現面向服務開發的架構
面向服務開發SOA的架構主要體現在表現層與應用層之間通過遠程通訊實現分離,表現層可以引用多方的應用服務作為基礎。由此系統實現業務上的分離,不同的功能模塊可以獨立開發,最后通過服務在表現層共同體現。長期的發展,使不少的企業針對單個功能模塊開發出一套獨立的系統,再通過強大的虛擬化技術為第三方提供服務,這就是云計算的前身。
就像一個通訊購物的平臺,其實就是綜合了內部業務管理、銀行轉帳服務、呼叫中心、第三方接口等多方服務的綜合性平臺。如果你有過這方面的經驗,就會知道其實銀行轉帳、呼叫中心不過就是銀行、電信、移動等公司提供的幾個簡單的接口。開發人員根本無需理會其實內部的結構,只要通過幾個簡單的遠程方法就能調用。這正是應用層服務 Application Service 的最好體現。
3. 結束語
寫這篇文章目的只是想與各位分享一下我在開發過程中的一些體會,歡迎各位點評,指出其中的不足。
其實架構是死物,人才是有腦子的生物。每一個架構必然會有其優點,也會有不足之處,我們應該從開發之中一齊起來體驗,而不是盲目地跟從,希望在下的拙見能夠給大家帶來幫助。可別忘了支持一下,挺一挺。
源代碼下載 (數據庫可以在.edmx文件根據模型生成)
相關文章
作者:風塵浪子
留言列表