WCF版的PetShop之一:PetShop簡介
系列文章導航:
WCF版的PetShop之三:實現分布式的Membership和上下文傳遞
在《WCF技術剖析(卷1)》的最后一章,我寫了一個簡單基于WCF的Web應用程序,該程序模擬一個最簡單的網上訂購的場景,所以我將其命名為PetShop。PetShop的目在于讓讀者體會到在真正的項目開發中,如何正確地、有效地使用WCF。在這個應用中,還會將個人對設計的一些總結融入其中,希望能夠對讀者有所啟發。Source Code從這里下載。
一、PetShop功能簡介
PetShop前端是一個單純的基于ASP.NET應用的Web站點,整個站點由以下三個Web頁面構成:
登錄頁面:和一般的基于Internet的Web站點一樣,采用基于用戶名/密碼的認證方式。在圖1所示的登錄頁面中,實際上僅僅使用了一個Login控件。熟悉ASP.NET的讀者應該很清楚,該控件和ASP.NET的成員資格(Membership)模塊進行了有效的集成,通過該模塊可進行用戶驗證。

圖1 PetShop登錄頁面
默認頁面:PetShop的默認頁面為一個寵物的列表,列表項包含寵物的編號、名稱、類別、價格、數量和相關介紹。登錄的用戶可以通過點擊“加入購物車”鏈接進行選購。默認頁面的界面如圖2所示。
圖2 PetShop默認頁面
購物車頁面:在用戶點擊默認頁面的“加入購物車”鏈接后,會跳轉到購物車頁面。如圖3所示,該頁面列出了當前登錄用戶購物車中選購的所有寵物列表。用戶可以將選購的寵物從購物車中移除,也可以更新選購的數量。
圖3 PetShop購物車頁面
嚴格來說,PetShop并不是一個功能完成的在線購物的Web應用,我們甚至沒有提供結帳的功能,功能的完整性并不是本案例關注的重點。接下來我們先討論一下整個PetShop的結構。
二、 PetShop的物理結構
PetShop采用典型的基于分布式的Web應用部署,從物理結構上講,大體上分為4個層次:客戶端(瀏覽器)、Web服務器(IIS)、應用服務器(IIS)和數據庫服務器。應用的前端展現,采用ASP.NET,整個ASP.NET Web站點部署于Web服務器的IIS中。ASP.NET Web應用本身并不承擔對主要業務邏輯的實現,也不直接與數據庫交互。PetShop將業務邏輯的實現定義在一個個WCF服務之中。WCF服務采用基于IIS的寄宿方式,部署于應用服務器。ASP.NET Web前端應用采用HTTP協議進行服務調用,如果兩者在同一個局域網內,可以采用TCP通信協議以獲得最好的性能,以及TCP協議本身提供的對可靠傳輸的支持。對數據庫的訪問發生在應用服務器與數據庫服務器之間。整個物理(部署)結構如圖4所示。
圖4 PetShop物理(部署)結構
三、PetShop的模塊劃分
模塊是應用最基本的組成單元,而模塊化是實現高內聚、松耦合的重要途徑。模塊本身應該是自治的,它獨立地承擔著某項功能的實現。模塊劃分應該是基于功能的,一個模塊可以看成是服務于某項功能的所有資源的集合,模塊的元素可以包括可視的UI、后臺代碼和SQL(或者存儲過程),以及存儲數據等。
1、模塊化設計
在進行團隊開發時,模塊之間的獨立性確保基于各個模塊的開發團隊可以獨立進行開發,對于大規模的應用開發,模塊化是保證軟件質量的重要途徑。模塊化對于測試也具有積極作用,因為模塊化賦予了每一個模塊“插件”的特質,單個模塊可以以“插件”的形式動態地插入現有系統,從而保證測試的及時交付。除了開發和測試,模塊化對于應用的部署及產品交付同樣重要。在時間就是金錢的今天,大多軟件的開發都是分階段進行的,每一個階段完成不同的模塊,階段性的成果需要及時向用戶交付。每次交付時,整個應用應該保持穩定的狀態。只有高度的模塊化,才能保證動態交付的模塊不會對現有的模塊造成影響。
模塊化以及由它帶來的好處,大部分人都能夠理解,但卻有很少人能夠正確地將其應用到實際的設計之中。很多人甚至沒有意識到,一些我們習以為常的設計違背了反模塊化的原則。舉一個很常見的例子,菜單對于大部分應用都是必須的,我們通常的做法是將整個應用的菜單內容統一維護,將它們保存到數據庫或XML中,當應用啟動的時候,整個菜單被加載顯示。對于應用的使用者來說,可視化的菜單結構反映應用當前能夠提供的可用功能的集合,如果基于某個模塊的菜單項能夠顯示出來,就應該保證相應模塊功能的完整性。但是,由于整個菜單的維護是獨立的,與模塊本身無關的,所以在測試的時候就會出現這樣的情形:整個菜單能夠很完整地顯示出來,但是隨便點擊某個菜單項,整個應用程序就崩潰。和開發人員聯系,得到的答案是相應的模塊尚未完成。這樣的設計對于部署也是不可取的,因為交付一個模塊,就需要對維護的菜單數據作一次修正。
如果按照模塊化的原則,整個設計應該是這樣:菜單的管理下放到具體的模塊中,當模塊加載的時候,模塊自行負責加載屬于自己的菜單,并添加到整個菜單樹相應的位置上。對于熟悉微軟軟件工廠(Software Factory)的讀者,應該知道微軟的-客戶端軟件工廠,無論是Web客戶端軟件工廠(WCSF:Web Client Software Factory)還是智能客戶端軟件工廠(SCSF:Smart Client Software Factory)對于菜單,都是采用這樣的設計模式。
模塊的自治特性并不意味著模塊之間不存在依賴,依賴在軟件設計中無所不在,設計的目標往往不是在于剔除依賴,而在于降低或者轉移依賴。一個模塊需要使用到另一個模塊提供的功能,依賴便產生了。依賴又可以分為運行時依賴和設計時(或者編譯時)依賴,我們關心的是如何降低設計時依賴,或者如何將設計時依賴轉移到運行時依賴。
對于模塊依賴來說,依賴方關心的是被依賴方能否提供它所需要的功能,而不關心被依賴方采用怎樣的手段去實現這些被依賴的功能。在面向對象的世界里,接口定義了一系列抽象的操作,從而制定了一份“契約”,實現了接口就相當于履行了這份契約,承諾實現接口定義的操作。所以,接口的本質就是對功能提供能力的描述,在設計時降低模塊依賴的最有效的途徑就是僅僅保留對接口的依賴。
對于模塊化的設計,如果一個模塊需要為別的模塊提供某種功能,我們需要為這些功能定義相應的接口。模塊自身提供對接口的實現,其他的模塊通過接口間接地消費被依賴模塊提供的功能。
2、業務模塊和基礎模塊
說到模塊,很多人首先想到的是對單一業務功能的實現,實際上這里所說的模塊僅僅是模塊的一種類型:業務模塊(Business Module)。除了實現某種業務功能外,還有一個模塊提供一些非業務功能的實現,比如異常處理(Exception Handling)、日志(Instrumentation)、審核(Auditing)、緩存(Caching)、事務處理(Transaction)等,我們可以把這些類型的模塊稱為基礎模塊(Foundation Module或Infrastructure Module)。基礎模塊為業務模塊提供一些公用的底層功能實現。
雖然模塊具有業務模塊和基礎模塊之分,在我看來,兩者并沒有本質的區別。雖然基礎模塊的主要任務就是為其他的模塊提供某種功能,注定處于被依賴一方,但是上層模塊調用基礎模塊的方式與調用其他業務模塊的方式并沒有本質的不同:都應該采用基于接口的調用方式。
3、PetShop的模塊劃分
雖然PetShop模擬的場景很簡單,但是為了演示模塊化的設計,特意將“簡單的問題復雜化”,將整個應用刻意地劃分列為以下兩個業務模塊:
- 產品模塊(Products):提供產品列表的獲取,以及向訂單模塊提供基于產品信息和庫存量的查詢。Products的接口定義在Products.Interface中;
- 訂單模塊(Orders):提供產品的訂購,由于該模塊在本例并不對其他模塊提供服務,所以并未為之定義接口。
除了以上兩個業務模塊之外,我將所有的基礎服務定義在Infrastructures項目中。在這里定義了兩個簡單的基礎服務:
- 導航服務:用于頁面之間的導航和參數傳遞的基礎服務;
- 查詢字符串解析服務:用于解析查詢字符串(QueryString)的基礎服務。
在這里,我多次提到“服務”二字,這與前面所介紹的WCF服務沒有關系。這里的服務為廣義的服務,指的是一個模塊為另一個模塊提供的功能,我們把模塊之間的調用也稱為服務調用。
圖5演示了整個PetShop解決方案的模塊劃分。基礎模塊定義在Infrastructures目錄下,上述的兩個業務模塊定義在Modules目錄的兩個子目錄Orders和Products下。DataBase目錄的包含一個Database項目,用于維護所有SQL腳本和存儲過程。Hosting對應一個IIS下的虛擬目錄,所有WCF服務項目編譯后的程序集都會生成到該目錄下的/Bin子目錄下,Hosting中還包括基于WCF服務的.svc文件。Common項目用于定義一些公用的類型。
圖5 從解決方案的結構看PetShop的模塊化設計
下面的代碼表示導航基礎服務的接口和實現,服務接口INavigatorService和NavigatorService分別定義在Infrastructures.Interface和Infrastructures項目下面。
1: using System.Collections.Generic;
2: namespace Artech.PetShop.Infrastructures.Interface
3: {
4: public interface INavigatorService
5: {
6: void Navigate(string targetUrl, IDictionary<string, object> prameters);
7: void Navigate(string targetUrl);
8: }
9: }
1: using System;
2: using System.Collections.Generic;
3: using System.Web;
4: using Artech.PetShop.Infrastructures.Interface;
5: namespace Artech.PetShop.Infrastructures
6: {
7: public class NavigatorService : INavigatorService
8: {
9: public void Navigate(string targetUrl, IDictionary<string, object> parameters)
10: {
11: if (string.IsNullOrEmpty(targetUrl))
12: {
13: throw new ArgumentNullException("targetUrl");
14: }
15: if (parameters == null)
16: {
17: throw new ArgumentNullException("prameters");
18: }
19:
20: if (parameters.Count == 0)
21: {
22: this.Navigate(targetUrl);
23: return;
24: }
25:
26: string queryString = string.Empty;
27: foreach (var parameter in parameters)
28: {
29: queryString += string.Format("{0}={1}&",parameter.Key, HttpUtility.UrlEncode(parameter.Value.ToString()));
30: }
31:
32: queryString = queryString.TrimEnd("&".ToCharArray());
33: HttpContext.Current.Response.Redirect(targetUrl + "?" + queryString);
34: }
35:
36: public void Navigate(string targetUrl)
37: {
38: if (string.IsNullOrEmpty(targetUrl))
39: {
40: throw new ArgumentNullException("targetUrl");
41: }
42: HttpContext.Current.Response.Redirect(targetUrl);
43: }
44: }
45: }
需要使用到基礎服務的模塊采用基于接口的服務調用方式,所以不須要引用到Infrastructures,僅僅須要引用Infrastructures.Interface,這無形之中降低了上層模塊與基礎模塊的依賴性。但是,基于基礎服務調用的編程又是如何定義的呢?基礎服務最終的實現定義在Infrastructures中,在運行時又是如何激活相應的基礎服務的呢?這就需要使用到我定義的另一個重要的靜態類型:ServiceLoader。ServiceLoader的實現采用了微軟P&P團隊開發的一個重要的應用程序塊Unity。Unity為我們提供了一個輕量級的、可擴展的依賴注入容器,關于Unity,在后面還會進行相應的介紹。ServiceLoader定義如下:
1: namespace Artech.PetShop.Common
2: {
3: public static class ServiceLoader
4: {
5: public static T LoadService()
6: {
7: //省略實現
8: }
9:
10: public static T LoadService(string serviceName)
11: {
12: //省略實現
13: }
14: }
15: }
借助ServiceLoader,我們就可以完全通過接口的方式對其他模塊的服務進行調用了,下面是通過INavigatorService調用導航服務的例子:
1: Dictionary<string, object> parameters = new Dictionary<string, object>();
2: parameters.Add("productid",001);
3: ServiceLoader.LoadService().Navigate("~/ShoppingCart.aspx", parameters);
對于需要向其他模塊提供服務的業務模塊來說,其定義方式和服務調用方式也和基礎模塊完全一樣。以Products模塊為例,它需要向Orders模塊提供基于產品的詳細信息,為此定義了ProductService和相應的接口IProduct(為了與后面定義的WCF服務契約IProductService相區別,在這里沒有加Service后綴)。IProduct定義在Products.Interface中,而ProductService定義在Products中。對ProductService的調用依然通過ServiceLoader采用基于接口的調用。下面的代碼提供了IProduct和ProductService的定義,以及借助ServiceLoader對該服務的調用。
1: using System;
2: using Artech.PetShop.Orders.BusinessEntity;
3: namespace Artech.PetShop.Products.Interface
4: {
5: public interface IProduct
6: {
7: Product GetProduct(Guid productID);
8: }
9: }
1: using System;
2: using Artech.PetShop.Common;
3: using Artech.PetShop.Orders.BusinessEntity;
4: using Artech.PetShop.Products.Interface;
5: using Artech.PetShop.Products.Service.Interface;
6: using Microsoft.Practices.EnterpriseLibrary.PolicyInjection.CallHandlers;
7: namespace Artech.PetShop.Products
8: {
9: [CachingCallHandler(0,30,0)]
10: public class ProductService : IProduct
11: {
12: private IProductService _proxy = ServiceProxyFactory.Create("productservice");
13:
14: #region IProduct Members
15: public Product GetProduct(Guid productID)
16: {
17: Product product = this._proxy.GetProductByID(productID);
18: if (product == null)
19: {
20: throw new BusinessException(string.Format("The product whose ID is \"{0}\" does not exist.", productID));
21: }
22:
23: return product;
24: }
25:
26: #endregion
27: }
28: }
調用方式:
1: Product product = ServiceLoader.LoadService().GetProduct(productID);
從上面的代碼可以看到,ProductService的實現需要調用WCF服務,并根據產品ID獲取產品信息。如果頻繁調用,必然對性能有很大的影響,產品信息是相對穩定的信息,所以可以通過緩存的機制改善應用程序的性能。在PetShop中,我們通過AOP的方式提供對緩存的實現。在此,使用到了微軟P&P團隊開發的另一個開源AOP框架:Policy Injection Application Block(PIAB)。通過PIAB,僅僅需要在目標類型或目標方法上應用CachingCallHandlerAttribute特性就可以了。CachingCallHandlerAttribute采用基于參數的緩存機制,它的實現原理是這樣的:當執行一個應用了CachingCallHandlerAttribute方法的時候,PIAB以傳入方法的參數列表為Key,判斷緩存中是否有相應的結果,如果有則直接返回而無須執行方法體;如果沒有執行方法體,將執行結果進行緩存。通過CachingCallHandlerAttribute還可以設置過期時間,在上面的例子中,將過期時間設為30分鐘([CachingCallHandler(0,30,0)])。關于PIAB,在后面還將進行簡單的介紹。