閱讀294 返回首頁    go 阿裏雲 go 技術社區[雲棲]


WCF版的PetShop之一:PetShop簡介[提供源代碼下載]

在《WCF技術剖析(卷1)》的最後一章,我寫了一個簡單基於WCF的Web應用程序,該程序模擬一個最簡單的網上訂購的場景,所以我將其命名為PetShop。PetShop的目在於讓讀者體會到在真正的項目開發中,如何正確地、有效地使用WCF。在這個應用中,還會將個人對設計的一些總結融入其中,希望能夠對讀者有所啟發。Source Code從這裏下載。

一、PetShop功能簡介

PetShop前端是一個單純的基於ASP.NET應用的Web站點,整個站點由以下三個Web頁麵構成:

登錄頁麵:和一般的基於Internet的Web站點一樣,采用基於用戶名/密碼的認證方式。在圖1所示的登錄頁麵中,實際上僅僅使用了一個Login控件。熟悉ASP.NET的讀者應該很清楚,該控件和ASP.NET的成員資格(Membership)模塊進行了有效的集成,通過該模塊可進行用戶驗證。

clip_image002

 圖1 PetShop登錄頁麵 

默認頁麵:PetShop的默認頁麵為一個寵物的列表,列表項包含寵物的編號、名稱、類別、價格、數量和相關介紹。登錄的用戶可以通過點擊“加入購物車”鏈接進行選購。默認頁麵的界麵如圖2所示。

clip_image004

圖2 PetShop默認頁麵

購物車頁麵:在用戶點擊默認頁麵的“加入購物車”鏈接後,會跳轉到購物車頁麵。如圖3所示,該頁麵列出了當前登錄用戶購物車中選購的所有寵物列表。用戶可以將選購的寵物從購物車中移除,也可以更新選購的數量。

clip_image006

圖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所示。

clip_image008

圖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項目用於定義一些公用的類型。

clip_image010

圖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<T>()
   6:         {
   7:             //省略實現
   8:         }
   9:  
  10:         public static T LoadService<T>(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<INavigatorService>().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<IProductService>("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<IProduct>().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,在後麵還將進行簡單的介紹。

 

WCF版的PetShop之二:模塊中的層次劃分[提供源代碼下載]

 


作者:蔣金楠
微信公眾賬號:大內老A
微博:www.weibo.com/artech
如果你想及時得到個人撰寫文章以及著作的消息推送,或者想看看個人推薦的技術資料,可以掃描左邊二維碼(或者長按識別二維碼)關注個人公眾號(原來公眾帳號蔣金楠的自媒體將會停用)。
本文版權歸作者和博客園共有,歡迎轉載,但未經作者同意必須保留此段聲明,且在文章頁麵明顯位置給出原文連接,否則保留追究法律責任的權利。
原文鏈接

最後更新:2017-10-30 11:04:34

  上一篇:go  【雲計算的1024種玩法】手把手教你如何編譯一個高性能 OpenResty
  下一篇:go  WCF版的PetShop之二:模塊中的層次劃分[提供源代碼下載]