走向ASP.NET架構設計——第三章:分層設計,初涉架構(前篇)
本篇主要講述ASP.NET應用中如何進行邏輯分層。本篇的前篇會從Smart UI 反模式和它的一些缺點開始講述,然后一步步的講述如何邏輯分層,而且在后篇中也會給出一個ASP.NET設計中常用的僅供參考的分層架構的Demo。
一個穩定和易維護的系統必須建立在一個好的基礎之上。計劃和設計一個好的架構對一個項目的成敗起著至關重要的作用。可能在我們一般做項目的時候,經驗告訴我們:3層,N層的設計,基本就能把問題解決了,很多的情況確實是這樣的。在提出一個設計的時候,常常要考慮為什么要這樣劃分結構,而且常常要承擔風險和責任,特別是萬一這個項目因為最初的設計而導致崩潰,那就郁悶了。所以設計的提出一定和考慮業務。
下面就先來看看Smart UI的設計方式。
Smart UI
想想我們最初是如何開發ASP.NET的應用的:在頁面設計界面中把界面布局好,然后雙擊控件就開始編寫功能代碼。很多的時候把邏輯判斷和數據訪問都寫在頁面的.cs的文件中。后來我們學習到了分層,逐漸的明白了這種方式的缺點:導致業務邏輯代碼到處分散而且重復,不利于以后的更改和維護等。
盡管有上述說的一些缺點,Smart UI還是有它的用途的,如為項目快速的建立一個原型或者開發一個功能比較的小的項目。還有一個問題,如何最初用Smart UI的方式開發的小項目很成功,慢慢的變大,變復雜了,那么很多的問題就出來了。就像Flower在架構模式一書中提到的:盡量用領域模型來組織一個項目的業務邏輯,盡管在開始的時候邏輯不復雜或者看不出這種方式的好處,一旦項目變化,好處就顯而易見了。在對項目原型開發中,盡量不用Smart UI。
其實Smart UI最大的問題就是:職責不清—把所有的東西全部寫在一起。為了和以后講述的內容的比較,我還是寫一個例子出來,很多朋友都已經對這種Smart UI的開發方式很熟悉了,可以跳過下面的例子。在例子中,我們會用電子商務中一個常見的場景:一個頁面來顯示一個產品的列表信息,如名字,推薦的零售價格(Recommend Retail Price),折扣,和庫存等。(如果朋友們愿意,可以照著下面的步驟一起做)
1. 打開Visual Studio,并且建立一個”空白的解決方案”,命名為:ASPPatterns.Chap3.SmartUI,然后添加一個新的Web項目,命名為:ASPPatterns.Chap3.SmartUI.Web.
2. 在新建的Web項目中右擊:Add—New Item,添加一個Sql Server的數據文件:Shop.mdf.
如下:
3. 在新加的數據庫文件上右擊,并且打開。然后添加一個新表:如下:
4. 添加一些測試的數據:
5. 然后選擇Products表,并且把表拖放到Default.aspx頁面上。這樣之后,在頁面上就自動添加一個GridView和SqlDataSource.
界面就如下圖:
6. 我我們添加額外的兩列來顯示折扣信息和庫存信息。Default.aspx的Source代碼最后如下:
7. 然后,我們在Default.aspx.cs后編碼:
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.UI;
using System.Web.UI.WebControls;
namespace ASPPatterns.Chap3.SmartUI.Web
{
public partial class Default : System.Web.UI.Page
{
protected void GridView1_RowDataBound(object sender, GridViewRowEventArgs e)
{
if (e.Row.RowType == DataControlRowType.DataRow)
{
decimal RRP = decimal.Parse(((System.Data.DataRowView)e.Row.DataItem)["RRP"].ToString());
decimal SellingPrice = decimal.Parse(((System.Data.DataRowView)e.Row.DataItem)["SellingPrice"].ToString());
Label lblSellingPrice = (Label)e.Row.FindControl("lblSellingPrice");
Label lblSavings = (Label)e.Row.FindControl("lblSavings");
Label lblDiscount = (Label)e.Row.FindControl("lblDiscount");
lblSavings.Text = DisplaySavings(RRP, ApplyExtraDiscountsTo(SellingPrice));
lblDiscount.Text = DisplayDiscount(RRP, ApplyExtraDiscountsTo(SellingPrice));
lblSellingPrice.Text = String.Format("{0:C}", ApplyExtraDiscountsTo(SellingPrice));
}
}
protected string DisplayDiscount(decimal RRP, decimal SalePrice)
{
string discountText = "";
if (RRP > SalePrice)
discountText = String.Format("{0:C}", (RRP - SalePrice));
return discountText;
}
protected string DisplaySavings(decimal RRP, decimal SalePrice)
{
string savingsTest = "";
if (RRP > SalePrice)
savingsTest = (1 - (SalePrice / RRP)).ToString("#%");
return savingsTest;
}
protected decimal ApplyExtraDiscountsTo(decimal OriginalSalePrice)
{
decimal price = OriginalSalePrice;
int discountType = Int16.Parse( this.ddlDiscountType.SelectedValue);
if (discountType == 1)
{
price = price * 0.95M;
}
return price;
}
protected void ddlDiscountType_SelectedIndexChanged(object sender, EventArgs e)
{
GridView1.DataBind();
}
}
}
在上面的 GridView1_RowDataBound方法在GridView的每個row被創建的時候調用。這個方法獲取每個產品的推薦的零售價格RRP(Recommend Retail Price),然后調用 DisplayDiscount和DisplaySavings方法來獲取折扣和庫存,然后再更新UI的顯示。
在上面的代碼中,就將計算折扣和計算庫存的邏輯寫在了UI中,而且數據的訪問代碼也寫在UI中了。這就意味著:如果我們想要在不同的頁面顯示產品的信息,那么這些邏輯就得一遍遍的重寫。如果我們在加一些新的功能,那么頁面后面的代碼就開始修改,開始縫縫補補。
解決Smart UI的方法就是劃分職責,我想大家都知道“單一職責的原則”,這個原則不僅僅適用于類,方法,而且對項目的層次劃分也有作用。分層,最主要的目的就是:把不通的功能放在各自對應的地方,這樣清晰的職責劃分,也是對變化點進行分離。
下面的圖就是一個典型的企業級ASP.NET項目的分層結構:
下面我們就來看看,按照我們的一般的分層的經驗來如何設計這功能:
1. 創建一個新的空白的解決方案,命名為ASPPatterns.Chap3.Layered.
2. 添加四個新的C#類庫,分別命名為:
a) ASPPatterns.Chap3.Layered.Repository.
b) ASPPatterns.Chap3.Layered.Model.
c) ASPPatterns.Chap3.Layered.Service.
d) ASPPatterns.Chap3.Layered.Presentation
3. 添加一個新的Web程序,命名為ASPPatterns.Chap3.Layered.Web.。
注:朋友們一眼就應該可以看出,這些類庫的命名是反映了一些DDD的一些概念,但是,不是說在一個項目的開發中用了這些概念名詞就表明就開發的方式是DDD了。
3. 不同的類庫就分別承擔不同的職責,而且每一層一般都只是引用自己的下一層,而且下一層不知道自己被上一層使用。本例中的引用關系如下:
這里我先提一下上面類庫的一起名字:盡管有關DDD和一些架構模式的概念我在以后的文章中會講,但我這里還是先給大家提一下,目的僅僅是讓大家對這個例子有一些更好的了解。
在DDD中,一直主張業務模型,也就是我們常常所說的業務類,例如之前例子中的Product,只關注自身的業務邏輯,而不管如何去獲取和保存數據,這些對數據的操作完全交給另外的對象去執行,也就是Repository,這樣就達到了DDD中所說的PI(Persistence Ignore)。所以在上面的例子中,ASPPatterns.Chap3.Layered.Model就代表了一個業務模型,它之所以被Repository引用,是因為Repository負責將Model的數據持久化到存儲設備中,而Model不管這些事情了。
在講ASPPatterns.Chap3.Layered.Service之前,首先給大家統一 一下Service的概念。
有時在類的設計過程中,有些行為不適合放在任何的一個類中,如果把這些行為放在一個不真正擁有它的類中,只能把類的職責搞混了。為了給這些行為一個安置的地方,我們常常把這些行為放在一個稱為服務的類中。
作為服務的類一般沒有狀態的,可以簡單的作為一個提供操作接口實現。
在DDD中,Service也是用來提供一種服務的。很多人看到了DDD的類層次結構是這樣的:Repository---Model---Service--- Presentation(包括本例),所以都以為Service只能出現在Model的上一層,如果看到Repository-- Service ---Model---Service--- Presentation這樣的層次結構,又作何感想。如果被這些所謂的結構搞迷惑了,那就說明對DDD的理解只是在于“形”上。Service就是向外部提供的功能接口,和我們常見的Web Service的概念很相似,例如的Web Service就是向外部系統提供一些功能的。
我們來看下面的一個圖:
有時候之所以要在Model層之上加上一個Service層,主要的原因就是實現粗顆粒度的API,往往和系統的User Case有一定的聯系。例如,如果在系統用例中要實現一個用戶訂單的處理,那么可能就涉及到Customer, Product,Order等類,當然,如果我們調用這些類來共同完成這個任務是沒問題的,但是這樣就向調用者暴露這些類之間的復雜的關系,而且如果處理的過程變化了,那么調用者的代碼就要改變,如果把這個處理的方法放在上面的任意一個類中,又顯得不倫不類,這里的Service功能就類似于設計模式中的Façade外觀模式。這樣就向外界提供簡單的API,向外界提供訂單處理的服務!
所以在一般在DDD中業務層被劃分為兩個邏輯層:Model (提供細粒度的業務邏輯處理,也便于重用), Service(提供業務處理的流程,提供粗顆粒度的供外部調用的方法)。
但是,我們常見到的Model層之上的Service層僅僅只是對CRUD的再次封裝,一個可能的原因就是業務不是很復雜,這時其實這個Service層可以拿掉的,但是考慮到以后可能邏輯會更多更復雜,所以還是保留Service這層。
其實在Repository上的那個Service也是同樣的概念。例如發送郵件通知用戶的功能。例如上圖中的最上層的Service可以調用業務層和基礎設施層的Service來共同完成一個事情。
今天的上篇的就講述到這里吧,下篇會用一個例子,代碼量還是有點的!敬請關注!
我先發上代碼,大家感興趣的看看,我們下篇講述!
留言列表