五 PetShop之業務邏輯層設計
業務邏輯層(Business Logic Layer)無疑是系統架構中體現核心價值的部分。它的關注點主要集中在業務規則的制定、業務流程的實現等與業務需求有關的系統設計,也即是說它是與系統所應對的領域(Domain)邏輯有關,很多時候,我們也將業務邏輯層稱為領域層。例如Martin Fowler在《Patterns of Enterprise Application Architecture》一書中,將整個架構分為三個主要的層:表示層、領域層和數據源層。作為領域驅動設計的先驅Eric Evans,對業務邏輯層作了更細致地劃分,細分為應用層與領域層,通過分層進一步將領域邏輯與領域邏輯的解決方案分離。
業務邏輯層在體系架構中的位置很關鍵,它處於數據訪問層與表示層中間,起到了數據交換中承上啟下的作用。由於層是一種弱耦合結構,層與層之間的依賴是向下的,底層對於上層而言是“無知”的,改變上層的設計對於其調用的底層而言沒有任何影響。如果在分層設計時,遵循了面向接口設計的思想,那麼這種向下的依賴也應該是一種弱依賴關系。因而在不改變接口定義的前提下,理想的分層式架構,應該是一個支持可抽取、可替換的“抽屜”式架構。正因為如此,業務邏輯層的設計對於一個支持可擴展的架構尤為關鍵,因為它扮演了兩個不同的角色。對於數據訪問層而言,它是調用者;對於表示層而言,它卻是被調用者。依賴與被依賴的關系都糾結在業務邏輯層上,如何實現依賴關系的解耦,則是除了實現業務邏輯之外留給設計師的任務。
5.1 與領域專家合作
設計業務邏輯層最大的障礙不在於技術,而在於對領域業務的分析與理解。很難想象一個不熟悉該領域業務規則和流程的架構設計師能夠設計出合乎客戶需求的系統架構。幾乎可以下定結論的是,業務邏輯層的設計過程必須有領域專家的參與。在我曾經參與開發的項目中,所涉及的領域就涵蓋了電力、半導體、汽車等諸多行業,如果缺乏這些領域的專家,軟件架構的設計尤其是業務邏輯層的設計就無從談起。這個結論唯一的例外是,架構設計師同時又是該領域的專家。然而,正所謂“千軍易得,一將難求”,我們很難尋覓到這樣卓越出眾的人才。
領域專家在團隊中扮演的角色通常稱為Business Consultor(業務咨詢師),負責提供與領域業務有關的咨詢,與架構師一起參與架構與數據庫的設計,撰寫需求文檔和設計用例(或者用戶故事User Story)。如果在測試階段,還應該包括撰寫測試用例。理想的狀態是,領域專家應該參與到整個項目的開發過程中,而不僅僅是需求階段。
領域專家可以是專門聘請的對該領域具有較深造詣的咨詢師,也可以是作為需求提供方的客戶。在極限編程(Extreme Programming)中,就將客戶作為領域專家引入到整個開發團隊中。它強調了現場客戶原則。現場客戶需要參與到計劃游戲、開發迭代、編碼測試等項目開發的各個階段。由於領域專家與設計師以及開發人員組成了一個團隊,貫穿開發過程的始終,就可以避免需求理解錯誤的情況出現。即使項目的開發與實際需求不符,也可以在項目早期及時修正,從而避免了項目不必要的延期,加強了對項目過程和成本的控制。正如Steve McConnell在構建活動的前期准備中提及的一個原則:發現錯誤的時間要盡可能接近引入該錯誤的時間。需求的缺陷在系統中潛伏的時間越長,代價就越昂貴。如果在項目開發中能夠與領域專家充分的合作,就可以最大效果地規避這樣一種惡性的鏈式反應。
傳統的軟件開發模型同樣重視與領域專家的合作,但這種合作主要集中在需求分析階段。例如瀑布模型,就非常強調早期計劃與需求調研。然而這種未雨綢缪的早期計劃方式,對架構師與需求調研人員的技能要求非常高,它強調需求文檔的精確性,一旦分析出現偏差,或者需求發生變更,當項目開發進入設計階段後,由於缺乏與領域專家溝通與合作的機制,開發人員估量不到這些錯誤與誤差,因而難以及時作出修正。一旦這些問題像毒瘤一般在系統中蔓延開來,逐漸暴露在開發人員面前時,已經成了一座難以逾越的高山。我們需要消耗更多的人力物力,才能夠修正這些錯誤,從而導致開發成本成數量級的增加,甚至於導致項目延期。當然還有一個好的選擇,就是放棄整個項目。這樣的例子不勝枚舉,事實上,項目開發的“滑鐵盧”,究其原因,大部分都是因為業務邏輯分析上出現了問題。
迭代式模型較之瀑布模型有很大地改進,因為它允許變更、優化系統需求,整個迭代過程實際上就是與領域專家的合作過程,通過向客戶演示迭代所產生的系統功能,從而及時獲取反饋,並逐一解決迭代演示中出現的問題,保證系統向著合乎客戶需求的方向演化。因而,迭代式模型往往能夠解決早期計劃不足的問題,它允許在發現缺陷的時候,在需求變更的時候重新設計、重新編碼並重新測試。
無論采用何種開發模型,與領域專家的合作都將成為項目成敗與否的關鍵。這基於一個軟件開發的普遍真理,那就是世界上沒有不變的需求。一句經典名言是:“沒有不變的需求,世上的軟件都改動過3次以上,唯一一個只改動過兩次的軟件的擁有者已經死了,死在去修改需求的路上。”一語道盡了軟件開發的殘酷與艱辛!
那麼應該如何加強與領域專家的合作呢?James Carey和Brent Carlson根據他們在參與的IBM SanFrancisco項目中獲得的經驗,提出了Innocent Questions模式,其意義即“改進領域專家和技術專家的溝通質量”。在一個項目團隊中,如果我們沒有一位既能擔任首席架構師,同時又是領域專家的人選,那麼加強領域專家與技術專家的合作就顯得尤為重要了。畢竟,作為一個領域專家而言,可能並不熟悉軟件設計方法學,也不具備面向對象開發和架構設計的能力,同樣,大部分技術專家很有可能對該項目所涉及的業務領域僅停留在一知半解的地步。如果領域專家與技術專家不能有效溝通,則整個項目的前途就岌岌可危了。
Innocent Questions模式提出的解決方案包括:
(1)選用可以與人和諧相處的人員組建開發團隊;
(2)清楚地定義角色和職權;
(3)明確定義需要的交互點;
(4)保持團隊緊密;
(5)雇傭優秀的人。
事實上,這已經從技術的角度上升到對團隊的管理層次了。就好比籃球運動一樣,即使你的球隊集合了五名世界上最頂尖最有天賦的球員,如果各自為戰,要想取得比賽的勝利依舊是非常困難的。團隊精神與權責分明才是取得勝利的保障,軟件開發同樣如此。
與領域專家合作的基礎是保證開發團隊中永遠保留至少一名領域專家。他可以是系統的客戶,第三方公司的咨詢師,最理想是自己公司雇傭的專家。如果項目中缺乏這樣的一個人,那麼我的建議是去雇傭他,如果你不想看到項目遭遇“西伯利亞寒流”的話。
確定領域專家的角色任務與職責。必須要讓團隊中的每一個人明確領域專家在整個團隊中究竟扮演什麼樣的角色,他的職責是什麼。一個合格的領域專家必須對業務領域有足夠深入的理解,他應該是一個能夠俯瞰整個系統需求、總攬全局的人物。在項目開發過程中,將由他負責業務規則和流程的制定,負責與客戶的溝通,需求的調研與討論,並於設計師一起參與系統架構的設計。編檔是領域專家必須參與的工作,無論是需求文檔還是設計文檔,以及用例的編寫,領域專家或者提出意見,或者作為撰寫的作者,至少他也應該是評審委員會的重要成員。
規范業務領域的術語和技術術語。領域專家和技術專家必須在保證不產生二義性的語義環境下進行溝通與交流。如果出現理解上的分歧,我們必須及時解決,通過討論確立術語標准。很難想象兩個語言不通的人能夠相互合作愉快,解決的辦法是加入一位翻譯人員。在領域專家與技術專家之間搭建一座語義上的橋梁,使其能夠相互理解、相互認同。還有一個辦法是在團隊內部開展培訓活動。尤其對於開發人員而言,或多或少地了解一些業務領域知識,對於項目的開發有很大的幫助。在我參與過的半導體領域的項目開發,團隊就專門邀請了半導體行業的專家就生產過程的業務邏輯進行了全方位的介紹與培訓。正所謂“磨刀不誤砍柴工”,雖然我們消費了培訓的時間,但對於掌握了業務規則與流程的開發人員,卻能夠提升項目開發進度,總體上節約了開發成本。
加強與客戶的溝通。客戶同時也可以作為團隊的領域專家,極限編程的現場客戶原則是最好的示例。但現實並不都如此的完美,在無法要求客戶成為開發團隊中的固定一員時,聘請或者安排一個專門的領域專家,加強與客戶的溝通,就顯得尤為重要。項目可以通過領域專家獲得客戶的及時反饋。而通過領域專家去了解變更了的需求,會在最大程度上減少需求誤差的可能。
5.2 業務邏輯層的模式應用
Martin Fowler在《企業應用架構模式》一書中對領域層(即業務邏輯層)的架構模式作了整體概括,他將業務邏輯設計分為三種主要的模式:Transaction Script、Domain Model和Table Module。
Transaction Script模式將業務邏輯看作是一個個過程,是比較典型的面向過程開發模式。應用Transaction Script模式可以不需要數據訪問層,而是利用SQL語句直接訪問數據庫。為了有效地管理SQL語句,可以將與數據庫訪問有關的行為放到一個專門的Gateway類中。應用Transaction Script模式不需要太多面向對象知識,簡單直接的特性是該模式全部價值之所在。因而,在許多業務邏輯相對簡單的項目中,應用Transaction Script模式較多。
Domain Model模式是典型的面向對象設計思想的體現。它充分考慮了業務邏輯的復雜多變,引入了Strategy模式等設計模式思想,並通過建立領域對象以及抽象接口,實現模式的可擴展性,並利用面向對象思想與身俱來的特性,如繼承、封裝與多態,用於處理復雜多變的業務邏輯。唯一制約該模式應用的是對象與關系數據庫的映射。我們可以引入ORM工具,或者利用Data Mapper模式來完成關系向對象的映射。
與Domain Model模式相似的是Table Module模式,它同樣具有面向對象設計的思想,唯一不同的是它獲得的對象並非是單純的領域對象,而是DataSet對象。如果為關系數據表與對象建立一個簡單的映射關系,那麼Domain Model模式就是為數據表中的每一條記錄建立一個領域對象,而Table Module模式則是將整個數據表看作是一個完整的對象。雖然利用DataSet對象會丟失面向對象的基本特性,但它在為表示層提供數據源支持方面卻有著得天獨厚的優勢。尤其是在.Net平台下,ADO.NET與Web控件都為Table Module模式提供了生長的肥沃土壤。
5.3 PetShop的業務邏輯層設計
PetShop在業務邏輯層設計中引入了Domain Model模式,這與數據訪問層對於數據對象的支持是分不開的。由於PetShop並沒有對寵物網上商店的業務邏輯進行深入,也省略了許多復雜細節的商務邏輯,因而在Domain Model模式的應用上並不明顯。最典型地應該是對Order領域對象的處理方式,通過引入Strategy模式完成對插入訂單行為的封裝。關於這一點,我已在第27章有了詳盡的描述,這裡就不再贅述。
本應是系統架構設計中最核心的業務邏輯層,由於簡化了業務流程的緣故,使得PetShop在這一層的設計有些乏善可陳。雖然在業務邏輯層中,針對B2C業務定義了相關的領域對象,但這些領域對象僅僅是完成了對數據訪問層中數據對象的簡單封裝而已,其目的僅在於分離層次,以支持對各種數據庫的擴展,同時將SQL語句排除在業務邏輯層外,避免了SQL語句的四處蔓延。
最能體現PetShop業務邏輯的除了對訂單的管理之外,還包括購物車(Shopping Cart)與Wish List的管理。在PetShop的BLL模塊中,定義了Cart類來負責相關的業務邏輯,定義如下:
[Serializable] public class Cart { private Dictionary cartItems = new Dictionary(); public decimal Total { get { decimal total = 0; foreach (CartItemInfo item in cartItems.Values) total += item.Price * item.Quantity; return total; } } public void SetQuantity(string itemId, int qty) { cartItems[itemId].Quantity = qty; } public int Count { get { return cartItems.Count; } } public void Add(string itemId) { CartItemInfo cartItem; if (!cartItems.TryGetValue(itemId, out cartItem)) { Item item = new Item(); ItemInfo data = item.GetItem(itemId); if (data != null) { CartItemInfo newItem = new CartItemInfo(itemId, data.ProductName, 1, (decimal)data.Price, data.Name, data.CategoryId, data.ProductId); cartItems.Add(itemId, newItem); } } else cartItem.Quantity++; } //其他方法略; }
Cart類通過一個Dictionary對象來負責對購物車內容的存儲,同時定義了Add、Remove、Clear等方法,來實現對購物車內容的管理。
在前面我提到PetShop業務邏輯層中的領域對象僅僅是完成對數據對象的簡單封裝,但這種分離層次的方法在架構設計中依然扮演了舉足輕重的作用。以Cart類的Add()方法為例,在方法內部引入了PetShop.BLL.Item領域對象,並調用了Item對象的GetItem()方法。如果沒有在業務邏輯層封裝Item對象,而是直接調用數據訪問層的Item數據對象,為保證層次間的弱依賴關系,就需要調用工廠對象的工廠方法來創建PetShop.IDAL.IItem接口類型對象。一旦數據訪問層的Item對象被多次調用,就會造成重復代碼,既不離於程序的修改與擴展,也導致程序結構生長為臃腫的態勢。
此外,領域對象對數據訪問層數據對象的封裝,也有利於表示層對業務邏輯層的調用。在三層式架構中,表示層應該是對於數據訪問層是“無知”的,這樣既減少了層與層間的依賴關系,也能有效避免“循環依賴”的後果。
值得商榷的是Cart類的Total屬性。其值的獲取是通過遍歷購物車集合,然後累加價格與商品數量的乘積。這裡顯然簡化了業務邏輯,而沒有充分考慮需求的擴展。事實上,這種獲取購物車總價格的算法,在大多數情況下僅僅是其中的一種策略而已,我們還應該考慮折扣的情況。例如,當總價格超過100元時,可以給與顧客一定的折扣,這是與網站的促銷計劃相關的。除了給與折扣的促銷計劃外,網站也可以考慮贈送禮品的促銷策略,因此我們有必要引入Strategy模式,定義接口IOnSaleStrategy:
public interface IOnSaleStrategy { decimal CalculateTotalPrice(Dictionary cartItems); }
如此一來,我們可以為Cart類定義一個有參數的構造函數:
private IOnSaleStrategy m_onSale; public Cart(IOnSaleStrategy onSale) { m_onSale = onSale; }
那麼Total屬性就可以修改為:
public decimal Total { get {return m_onSale.CalculateTotalPrice(cartItems);} }
如此一來,就可以使得Cart類能夠有效地支持網站推出的促銷計劃,也符合開-閉原則。同樣的,這種設計方式也是Domain Model模式的體現。修改後的設計如圖5-1所示:
圖5-1 引入Strategy模式
作為一個B2C的電子商務架構,它所涉及的業務領域已為大部分設計師與開發人員所熟悉,因而在本例中,與領域專家的合作顯得並不那麼重要。然而,如果我們要開發一個成功的電子商務網站,與領域專家的合作仍然是必不可少的。以訂單的管理而言,如果考慮復雜的商業應用,就需要管理訂單的跟蹤(Tracking),與網上銀行的合作,賬戶安全性,庫存管理,物流管理,以及客戶關系管理(CRM)。整個業務過程卻涵蓋了諸如電子商務、銀行、物流、客戶關系學等諸多領域,如果沒有領域專家的參與,業務邏輯層的設計也許會“敗走麥城”。
5.4 與數據訪問層的通信
業務邏輯層需要與數據訪問層通信,利用數據訪問層訪問數據庫,因此業務邏輯層與數據訪問層之間就存在依賴關系。在數據訪問層引入接口程序集以及數據工廠的設計前提下,能夠做到兩者間關系為弱依賴。我們從業務邏輯層的引用程序集中可以看到,BLL模塊並沒有引用SQLServerDAL和OracleDAL程序集。在業務邏輯層中,有關數據訪問層中數據對象的調用,均利用多態原理定義了抽象的接口類型對象,然後利用工廠對象的工廠方法創建具體的數據對象。如PetShop.BLL.PetShop領域對象所示:
namespace PetShop.BLL { public class Product { //根據工廠對象創建IProduct接口類型實例; private static readonly IProduct dal = PetShop.DALFactory.DataAccess.CreateProduct(); //調用IProduct對象的接口方法GetProductByCategory(); public IList GetProductsByCategory(string category) { // 如果為空則新建List對象; if(string.IsNullOrEmpty(category)) return new List (); // 通過數據訪問層的數據對象訪問數據庫; return dal.GetProductsByCategory(category); } //其他方法略; } }
在領域對象Product類中,利用數據訪問層的工廠類DALFactory.DataAccess創建PetShop.IDAL.IProduct類型的實例,如此就可以解除對具體程序集SQLServerDAL或OracleDAL的依賴。只要PetShop.IDAL的接口方法不變,即使修改了IDAL接口模塊的具體實現,都不會影響業務邏輯層的實現。這種松散的弱耦合關系,才能夠最大程度地支持架構的可擴展。
領域對象Product實際上還完成了對數據對象Product的封裝,它們暴露在外的接口方法是一致地,正是通過封裝,使得表示層可以完全脫離數據庫以及數據訪問層,表示層的調用者僅需要關注業務邏輯層的實現邏輯,以及領域對象暴露的接口和調用方式。事實上,只要設計合理,規范了各個層次的接口方法,三層式架構的設計完全可以分離開由不同的開發人員同時開發,這就可以有效地利用開發資源,縮短項目開發周期。
5.5 面向接口設計
也許是業務邏輯比較簡單地緣故,在業務邏輯層的設計中,並沒有秉承在數據訪問層中面向接口設計的思想。除了完成對插入訂單策略的抽象外,整個業務邏輯層僅以BLL模塊實現,沒有為領域對象定義抽象的接口。因而PetShop的表示層與業務邏輯層就存在強依賴關系,如果業務邏輯層中的需求發生變更,就必然會影響表示層的實現。唯一可堪欣慰的是,由於我們采用分層式架構將用戶界面與業務領域邏輯完全分離,一旦用戶界面發生更改,例如將B/S架構修改為C/S架構,那麼業務邏輯層的實現模塊是可以完全重用的。
然而,最理想的方式仍然是面向接口設計。根據第28章對ASP.NET緩存的分析,我們可以將表示層App_Code下的Proxy類與Utility類劃分到業務邏輯層中,並修改這些靜態類為實例類,並將這些類中與業務領域有關的方法抽象為接口,然後建立如數據訪問層一樣的抽象工廠。通過“依賴注入”方式,解除與具體領域對象類的依賴,使得表示層僅依賴於業務邏輯層的接口程序集以及工廠模塊。
那麼,這樣的設計是否有“過度設計”的嫌疑呢?我們需要依據業務邏輯的需求情況而定。此外,如果我們需要引入緩存機制,為領域對象創建代理類,那麼為領域對象建立接口,就顯得尤為必要。我們可以建立一個專門的接口模塊IBLL,用以定義領域對象的接口。以Product領域對象為例,我們可以建立IProduct接口:
public interface IProduct { IList GetProductByCategory(string category); IList GetProductByCategory(string[] keywords); ProductInfo GetProduct(string productId); }
在BLL模塊中可以引入對IBLL程序集的依賴,則領域對象Product的定義如下:
public class Product:IProduct { public IList GetProductByCategory(string category) { //實現略; } public IList GetProductByCategory(string[] keywords) { //實現略; } public ProductInfo GetProduct(string productId) { //實現略; } }
然後我們可以為代理對象建立專門的程序集BLLProxy,它不僅引入對IBLL程序集的依賴,同時還將依賴於BLL程序集。此時代理對象ProductDataProxy的定義如下:
using PetShop.IBLL; using PetShop.BLL; namespace PetShop.BLLProxy { public class ProductDataProxy:IProduct { public IList GetProductByCategory(string category) { Product product = new Product(); //其他實現略; } public IList GetProductByCategory(string[] keywords) { //實現略; } public ProductInfo GetProduct(string productId) { //實現略; } } }
如此的設計正是典型的Proxy模式,其類結構如圖5-2所示:
圖5-2 Proxy模式
參照數據訪問層的設計方法,我們可以為領域對象及代理對象建立抽象工廠,並在web.config中配置相關的配置節,然後利用反射技術創建具體的對象實例。如此一來,表示層就可以僅僅依賴PetShop.IBLL程序集以及工廠模塊,如此就可以解除表示層與具體領域對象之間的依賴關系。表示層與修改後的業務邏輯層的關系如圖5-3所示:
圖5-3 修改後的業務邏輯層與表示層的關系
圖5-4則是PetShop 4.0原有設計的層次關系圖:
圖5-4 PetShop 4.0中表示層與業務邏輯層的關系
通過比較圖5-3與圖5-4,雖然後者不管是模塊的個數,還是模塊之間的關系,都相對更加簡單,然而Web Component組件與業務邏輯層之間卻是強耦合的,這樣的設計不利於應對業務擴展與需求變更。通過引入接口模塊IBLL與工廠模塊BLLFactory,解除了與具體模塊BLL的依賴關系。這種設計對於業務邏輯相對比較復雜的系統而言,更符合面向對象的設計思想,有利於我們建立可抽取、可替換的“抽屜”式三層架構。
以上就是PetShop的業務邏輯層設計全部內容,希望能給大家一個參考,也希望大家多多支持。