《解剖PetShop》系列之五
五 PetShop之商務邏輯層設計
商務邏輯層(Business Logic Layer)無疑是系統架構中體現核心價值的部分。它的關注點主要集中在商務規則的制定、商務程序的實現等與業務需求有關的系統設計,也即是說它是與系統所應對的領域(Domain)邏輯有關,很多時候,我們也將商務邏輯層稱為領域層。例如Martin Fowler在《Patterns of Enterprise Application Architecture》一書中,將整個架構分為三個主要的層:展示層、領域層和資料來源層。作為領域驅動設計的先驅Eric Evans,對商務邏輯層作了更細緻地劃分,細分為應用程式層與領域層,通過分層進一步將領域邏輯與領域邏輯的解決方案分離。
商務邏輯層在體系架構中的位置很關鍵,它處於資料訪問層與展示層中間,起到了資料交換中承上啟下的作用。由於層是一種弱耦合結構,層與層之間的依賴是向下的,底層對於上層而言是“無知”的,改變上層的設計對於其調用的底層而言沒有任何影響。如果在分層設計時,遵循了面向介面設計的思想,那麼這種向下的依賴也應該是一種弱依賴關係。因而在不改變介面定義的前提下,理想的分層式架構,應該是一個支援可抽取、可替換的“抽屜”式架構。正因為如此,商務邏輯層的設計對於一個支援可擴充的架構尤為關鍵,因為它扮演了兩個不同的角色。對於資料訪問層而言,它是調用者;對於展示層而言,它卻是被調用者。依賴與被依賴的關係都糾結在商務邏輯層上,如何?依賴關係的解耦,則是除了實現商務邏輯之外留給設計師的任務。
5.1 與領域專家合作
設計商務邏輯層最大的障礙不在於技術,而在於對領域業務的分析與理解。很難想象一個不熟悉該領域商務規則和流程的架構設計師能夠設計出合乎客戶需求的系統架構。幾乎可以下定結論的是,商務邏輯層的設計過程必須有領域專家的參與。在我曾經參與開發的項目中,所涉及的領域就涵蓋了電力、半導體、汽車等諸多行業,如果缺乏這些領域的專家,軟體架構的設計尤其是商務邏輯層的設計就無從談起。這個結論唯一的例外是,架構設計師同時又是該領域的專家。然而,正所謂“千軍易得,一將難求”,我們很難尋覓到這樣卓越出眾的人才。
領域專家在團隊中扮演的角色通常稱為Business Consultor(業務諮詢師),負責提供與領域業務有關的諮詢,與架構師一起參與架構與資料庫的設計,撰寫需求文檔和設計用例(或者使用者故事User Story)。如果在測試階段,還應該包括撰寫測試案例。理想的狀態是,領域專家應該參與到整個項目的開發過程中,而不僅僅是需求階段。
領域專家可以是專門聘請的對該領域具有較深造詣的諮詢師,也可以是作為需求提供方的客戶。在極限編程(Extreme Programming)中,就將客戶作為領域專家引入到整個Team Dev中。它強調了現場客戶原則。現場客戶需要參與到計劃遊戲、開發迭代、編碼測試等項目開發的各個階段。由於領域專家與設計師以及開發人員組成了一個團隊,貫穿開發過程的始終,就可以避免需求理解錯誤的情況出現。即使項目的開發與實際需求不符,也可以在項目早期及時修正,從而避免了項目不必要的延期,加強了對項目過程和成本的控制。正如Steve McConnell在構建活動的前期準備中提及的一個原則:發現錯誤的時間要儘可能接近引入該錯誤的時間。需求的缺陷在系統中潛伏的時間越長,代價就越昂貴。如果在項目開發中能夠與領域專家充分的合作,就可以最大效果地規避這樣一種惡性的鏈式反應。
傳統的軟體開發模型同樣重視與領域專家的合作,但這種合作主要集中在需求分析階段。例如瀑布模型,就非常強調早期計劃與需求調研。然而這種未雨綢繆的早期計劃方式,對架構師與需求調研人員的技能要求非常高,它強調需求文檔的精確性,一旦分析出現偏差,或者需求發生變更,當項目開發進入設計階段後,由於缺乏與領域專家溝通與合作的機制,開發人員估量不到這些錯誤與誤差,因而難以及時作出修正。一旦這些問題像毒瘤一般在系統中蔓延開來,逐漸暴露在開發人員面前時,已經成了一座難以逾越的高山。我們需要消耗更多的人力物力,才能夠修正這些錯誤,從而導致開發成本成數量級的增加,甚至於導致項目延期。當然還有一個好的選擇,就是放棄整個項目。這樣的例子不勝枚舉,事實上,項目開發的“滑鐵盧”,究其原因,大部分都是因為商務邏輯分析上出現了問題。
迭代式模型較之瀑布模型有很大地改進,因為它允許變更、最佳化系統需求,整個迭代過程實際上就是與領域專家的合作過程,通過向客戶示範迭代所產生的系統功能,從而及時擷取反饋,並逐一解決迭代示範中出現的問題,保證系統向著合乎客戶需求的方向演化。因而,迭代式模型往往能夠解決早期計劃不足的問題,它允許在發現缺陷的時候,在需求變更的時候重新設計、重新編碼並重新測試。
無論採用何種開發模型,與領域專家的合作都將成為項目成敗與否的關鍵。這基於一個軟體開發的普遍真理,那就是世界上沒有不變的需求。一句經典名言是:“沒有不變的需求,世上的軟體都改動過3次以上,唯一一個只改動過兩次的軟體的擁有者已經死了,死在去修改需求的路上。”一語道盡了軟體開發的殘酷與艱辛!
那麼應該如何加強與領域專家的合作呢?James Carey和Brent Carlson根據他們在參與的IBM SanFrancisco項目中獲得的經驗,提出了Innocent Questions模式,其意義即“改進領域專家和技術專家的溝通品質”。在一個項目團隊中,如果我們沒有一位既能擔任首席架構師,同時又是領域專家的人選,那麼加強領域專家與技術專家的合作就顯得尤為重要了。畢竟,作為一個領域專家而言,可能並不熟悉軟體設計方法學,也不具備物件導向開發和架構設計的能力,同樣,大部分技術專家很有可能對該項目所涉及的業務領域僅停留在一知半解的地步。如果領域專家與技術專家不能有效溝通,則整個項目的前途就岌岌可危了。
Innocent Questions模式提出的方案套件括:
(1)選用可以與人和諧相處的人員組建Team Dev;
(2)清楚地定義角色和職權;
(3)明確定義需要的互動點;
(4)保持團隊緊密;
(5)僱傭優秀的人。
事實上,這已經從技術的角度上升到對團隊的管理層次了。就好比籃球運動一樣,即使你的球隊集合了五名世界上最頂尖最有天賦的球員,如果各自為戰,要想取得比賽的勝利依舊是非常困難的。團隊精神與權責分明才是取得勝利的保障,軟體開發同樣如此。
與領域專家合作的基礎是保證Team Dev中永遠保留至少一名領域專家。他可以是系統的客戶,第三方公司的諮詢師,最理想是自己公司僱傭的專家。如果項目中缺乏這樣的一個人,那麼我的建議是去僱傭他,如果你不想看到項目遭遇“西伯利亞寒流”的話。
確定領域專家的角色任務與職責。必須要讓團隊中的每一個人明確領域專家在整個團隊中究竟扮演什麼樣的角色,他的職責是什麼。一個合格的領域專家必須對業務領域有足夠深入的理解,他應該是一個能夠俯瞰整個系統需求、總攬全域的人物。在項目開發過程中,將由他負責商務規則和流程的制定,負責與客戶的溝通,需求的調研與討論,並於設計師一起參與系統架構的設計。編檔是領域專家必須參與的工作,無論是需求文檔還是設計文檔,以及用例的編寫,領域專家或者提出意見,或者作為撰寫的作者,至少他也應該是評審委員會的重要成員。
規範業務領域的術語和技術術語。領域專家和技術專家必須在保證不產生二義性的語義環境下進行溝通與交流。如果出現理解上的分歧,我們必須及時解決,通過討論確立術語標準。很難想象兩個語言不通的人能夠相互合作愉快,解決的辦法是加入一位翻譯人員。在領域專家與技術專家之間搭建一座語義上的橋樑,使其能夠相互理解、相互認同。還有一個辦法是在團隊內部開展培訓活動。尤其對於開發人員而言,或多或少地瞭解一些業務領域知識,對於項目的開發有很大的協助。在我參與過的半導體領域的項目開發,團隊就專門邀請了半導體行業的專家就生產過程的商務邏輯進行了全方位的介紹與培訓。正所謂“磨刀不誤砍柴工”,雖然我們消費了培訓的時間,但對於掌握了商務規則與流程的開發人員,卻能夠提升項目開發進度,總體上節約了開發成本。
加強與客戶的溝通。客戶同時也可以作為團隊的領域專家,極限編程的現場客戶原則是最好的樣本。但現實並不都如此的完美,在無法要求客戶成為Team Dev中的固定一員時,聘請或者安排一個專門的領域專家,加強與客戶的溝通,就顯得尤為重要。項目可以通過領域專家獲得客戶的及時反饋。而通過領域專家去瞭解變更了的需求,會在最大程度上減少需求誤差的可能。
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資料對象,為保證層次間的弱依賴關係,就需要調用工廠對象的Factory 方法來建立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程式集。在商務邏輯層中,有關資料訪問層中資料對象的調用,均利用多態原理定義了抽象的介面類型對象,然後利用工廠對象的Factory 方法建立具體的資料對象。如PetShop.BLL.PetShop領域對象所示:
namespace PetShop.BLL
{
public class Product
{
//根據工廠對象建立IProduct介面類型執行個體;
private static readonly IProduct dal = PetShop.DALFactory.DataAccess.CreateProduct();
//調用IProduct對象的介面方法GetProductByCategory();
public IListGetProductsByCategory(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
{
IListGetProductByCategory(string category);
IListGetProductByCategory(string[] keywords);
ProductInfo GetProduct(string productId);
}
在BLL模組中可以引入對IBLL程式集的依賴,則領域對象Product的定義如下:
public class Product:IProduct
{
public IListGetProductByCategory(string category) { //實現略; }
public IListGetProductByCategory(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 IListGetProductByCategory(string category)
{
Product product = new Product();
//其他實現略;
}
public IListGetProductByCategory(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的依賴關係。這種設計對於商務邏輯相對比較複雜的系統而言,更符合物件導向的設計思想,有利於我們建立可抽取、可替換的“抽屜”式三層架構。