1、
PetShop
的系統架構設計
在軟件體系架構設計中,分層式結構是最多見,也是最重要的一種結構。微軟推薦的分層式結構通常分爲三層,從下至上分別爲:數據訪問層、業務邏輯層(又或成爲領域層)、表示層,如圖所看到的:
圖一:三層的分層式結構
數據訪問層:有時候也稱爲是持久層,其功能主要是負責數據庫的訪問。簡單的說法就是實現對數據表的Select,Insert,Update,Delete的操做。假設要添加ORM的元素,那麼就會包含對象和數據表之間的mapping,以及對象實體的持久化。在PetShop的數據訪問層中,並無使用ORM,從而致使了代碼量的添加,可以看做是整個設計實現中的一大敗筆。
業務邏輯層:是整個系統的核心,它與這個系統的業務(領域)有關。以PetShop爲例,業務邏輯層的相關設計,均和網上寵物店特有的邏輯相關,好比查詢寵物,下訂單,加入寵物到購物車等等。假設涉及到數據庫的訪問,則調用數據訪問層。
表示層:是系統的UI部分,負責使用者與整個系統的交互。在這一層中,理想的狀態是不該包括系統的業務邏輯。表示層中的邏輯代碼,僅與界面元素有關。在PetShop中,是利用ASP.Net來設計的,所以包括了不少Web控件和相關邏輯。
分層式結構到底其優點何在?Martin Fowler在《Patterns of Enterprise Application Architecture》一書中給出了答案:
一、開發者可以僅僅關注整個結構中的當中某一層;
二、可以很是easy的用新的實現來替換原有層次的實現;
三、可以減小層與層之間的依賴;
四、有利於標準化;
五、利於各層邏輯的複用。
歸納來講,分層式設計可以達至例如如下目的:分散關注、鬆散耦合、邏輯複用、標準定義。
一個好的分層式結構,可以使得開發者的分工更加明白。一旦定義好各層次之間的接口,負責不一樣邏輯設計的開發者就可以分散關注,齊頭並進。好比UI人員僅僅需考慮用戶界面的體驗與操做,領域的設計人員可以僅關注業務邏輯的設計,而數據庫設計人員也沒必要爲繁瑣的用戶交互而頭疼了。每個開發者的任務獲得了確認,開發進度就可以迅速的提升。
鬆散耦合的優勢是顯而易見的。假設一個系統沒有分層,那麼各自的邏輯都牢牢糾纏在一塊兒,彼此間相互依賴,誰都是不可替換的。一旦發生改變,則牽一髮而動全身,對項目的影響極爲嚴重。減小層與層間的依賴性,既可以良好地保證將來的可擴展,在複用性上也是優點明顯。每個功能模塊一旦定義好統一的接口,就可以被各個模塊所調用,而不用爲一樣的功能進行反覆地開發。
進行好的分層式結構設計,標準也是不可缺乏的。僅僅有在必定程度的標準化基礎上,這個系統纔是可擴展的,可替換的。而層與層之間的通訊也一定保證了接口的標準化。
「金無足赤,人無完人」,分層式結構也不可避免具備一些缺陷:
一、減小了系統的性能。這是不言而喻的。假設不採用分層式結構,很是多業務可以直接造訪數據庫,以此獲取對應的數據,如今卻必須經過中間層來完畢。
二、有時會致使級聯的改動。這樣的改動尤爲體如今自上而下的方向。假設在表示層中需要添加一個功能,爲保證其設計符合分層式結構,可能需要在對應的業務邏輯層和數據訪問層中都添加對應的代碼。
前面提到,PetShop的表示層是用ASP.Net設計的,也就是說,它應是一個BS系統。在.Net中,標準的BS分層式結構例如如下圖所看到的:
圖二:.Net中標準的BS分層式結構
隨着PetShop版本號的更新,其分層式結構也在不斷的無缺,好比PetShop2.0,就沒有採用標準的三層式結構,如圖三:
圖三:PetShop 2.0的體系架構
從圖中咱們可以看到,並無明顯的數據訪問層設計。這種設計儘管提升了數據訪問的性能,但也同一時候致使了業務邏輯層與數據訪問的職責混亂。一旦要求支持的數據庫發生變化,或者需要改動數據訪問的邏輯,由於沒有清晰的分層,會致使項目做大的改動。而隨着硬件系統性能的提升,以及充分利用緩存、異步處理等機制,分層式結構所帶來的性能影響差點兒可以忽略不計。
PetShop3.0糾正了此前層次不明的問題,將數據訪問邏輯做爲單獨的一層獨立出來:
圖四:PetShop 3.0的體系架構
PetShop4.0基本上延續了3.0的結構,但在性能上做了必定的改進,引入了緩存和異步處理機制,同一時候又充分利用了ASP.Net 2.0的新功能MemberShip,所以PetShop4.0的系統架構圖例如如下所看到的:
圖五:PetShop 4.0的體系架構
比較3.0和4.0的系統架構圖,其核心的內容並無發生變化。在數據訪問層(DAL)中,仍然採用DAL Interface抽象出數據訪問邏輯,並以DAL Factory做爲數據訪問層對象的工廠模塊。對於DAL Interface而言,分別有支持MS-SQL的SQL Server DAL和支持Oracle的Oracle DAL具體實現。而Model模塊則包括了數據實體對象。其具體的模塊結構圖例如如下所看到的:
圖六:數據訪問層的模塊結構圖
可以看到,在數據訪問層中,全然採用了「面向接口編程」思想。抽象出來的IDAL模塊,脫離了與詳細數據庫的依賴,從而使得整個數據訪問層利於數據庫遷移。DALFactory模塊專門管理DAL對象的建立,便於業務邏輯層訪問。SQLServerDAL和OracleDAL模塊均實現IDAL模塊的接口,當中包括的邏輯就是對數據庫的Select,Insert,Update和Delete操做。因爲數據庫類型的不一樣,對數據庫的操做也有所不一樣,代碼也會所以有所差異。
此外,抽象出來的IDAL模塊,除了解除了向下的依賴以外,對於其上的業務邏輯層,相同僅存在弱依賴關係,例如如下圖所看到的:
圖七:業務邏輯層的模塊結構圖
圖七中BLL是業務邏輯層的核心模塊,它包括了整個系統的核心業務。在業務邏輯層中,不能直接訪問數據庫,而必須經過數據訪問層。注意圖中對數據訪問業務的調用,是經過接口模塊IDAL來完畢的。既然與詳細的數據訪問邏輯無關,則層與層之間的關係就是鬆散耦合的。假設此時需要改動數據訪問層的詳細實現,僅僅要不涉及到IDAL的接口定義,那麼業務邏輯層就不會受到不論什麼影響。畢竟,詳細實現的SQLServerDAL和OracalDAL根本就與業務邏輯層沒有半點關係。
因爲在PetShop 4.0中引入了異步處理機制。插入訂單的策略可以分爲同步和異步,二者的插入策略明顯不一樣,但對於調用者而言,插入訂單的接口是全然同樣的,因此PetShop 4.0中設計了IBLLStrategy模塊。儘管在IBLLStrategy模塊中,不過簡單的IOrderStategy,但同一時候也給出了一個範例和信息,那就是在業務邏輯的處理中,假設存在業務操做的多樣化,或者是從此可能的變化,均應利用抽象的原理。或者使用接口,或者使用抽象類,從而脫離對詳細業務的依賴。不過在PetShop中,因爲業務邏輯相對簡單,這樣的思想體現得不夠明顯。也正因爲此,PetShop將核心的業務邏輯都放到了一個模塊BLL中,並無將詳細的實現和抽象嚴格的依照模塊分開。因此表示層和業務邏輯層之間的調用關係,其耦合度相對較高:
圖八:表示層的模塊結構圖
在圖五中,各個層次中還引入了輔助的模塊,如數據訪問層的Messaging模塊,是爲異步插入訂單的功能提供,採用了MSMQ(Microsoft Messaging Queue)技術。而表示層的CacheDependency則提供緩存功能。這些特殊的模塊,我會在此後的文章中具體介紹。
2、PetShop數據訪問層之數據庫訪問設計
在系列一中,我從整體上分析了PetShop的架構設計,並說起了分層的概念。從本部分開始,我將依次對各層進行代碼級的分析,以求得到更加仔細而深刻的理解。在PetShop 4.0中,由於引入了ASP.Net 2.0的一些新特點,因此數據層的內容也更加的普遍和複雜,包含:數據庫訪問、Messaging、MemberShip、Profile四部分。在系列二中,我將介紹有關數據庫訪問的設計。
在PetShop中,系統需要處理的數據庫對象分爲兩類:一是數據實體,相應數據庫中相應的數據表。它們沒有行爲,僅用於表現對象的數據。這些實體類都被放到Model程序集中,好比數據表Order相應的實體類OrderInfo,其類圖例如如下:
這些對象並不具備持久化的功能,簡單地說,它們是做爲數據的載體,便於業務邏輯針對相應數據表進行讀/寫操做。儘管這些類的屬性分別映射了數據表的列,而每一個對象實例也偏偏相應於數據表的每一行,但這些實體類卻並不具有相應的數據庫訪問能力。
由於數據訪問層和業務邏輯層都將對這些數據實體進行操做,所以程序集Model會被這兩層的模塊所引用。
第二類數據庫對象則是數據的業務邏輯對象。這裏所指的業務邏輯,並非業務邏輯層意義上的領域(domain)業務邏輯(從這個意義上,我更傾向於將業務邏輯層稱爲「領域邏輯層」),通常意義上說,這些業務邏輯即爲主要的數據庫操做,包含Select,Insert,Update和Delete。由於這些業務邏輯對象,僅具備行爲而與數據無關,所以它們均被抽象爲一個單獨的接口模塊IDAL,好比數據表Order相應的接口IOrder:
將數據實體與相關的數據庫操做分離出來,符合面向對象的精神。首先,它體現了「職責分離」的原則。將數據實體與其行爲分開,使得二者之間依賴減弱,當數據行爲發生改變時,並不影響Model模塊中的數據實體對象,避免了因一個類職責過多、過大,從而致使該類的引用者發生「災難性」的影響。其次,它體現了「抽象」的精神,或者說是「面向接口編程」的最佳體現。抽象的接口模塊IDAL,與詳細的數據庫訪問實現全然隔離。這樣的與實現無關的設計,保證了系統的可擴展性,同一時候也保證了數據庫的可移植性。在PetShop中,可以支持SQL Server和Oracle,那麼它們詳細的實現就分別放在兩個不一樣的模塊SQLServerDAL、OracleDAL中。
以Order爲例,在SQLServerDAL、OracleDAL兩個模塊中,有不一樣的實現,但它們同一時候又都實現了IOrder接口,如圖:
從數據庫的實現來看,PetShop體現出了沒有ORM框架的臃腫與醜陋。由於要對數據表進行Insert和Select操做,以SQL Server爲例,就使用了SqlCommand,SqlParameter,SqlDataReader等對象,以完畢這些操做。尤爲複雜的是Parameter的傳遞,在PetShop中,使用了大量的字符串常量來保存參數的名稱。此外,PetShop還專門爲SQL Server和Oracle提供了抽象的Helper類,包裝了一些常用的操做,如ExecuteNonQuery、ExecuteReader等方法。
在沒有ORM的狀況下,使用Helper類是一個比較好的策略,利用它來完畢數據庫基本操做的封裝,可以下降很是多和數據庫操做有關的代碼,這體現了對象複用的原則。PetShop將這些Helper類統一放到DBUtility模塊中,不一樣數據庫的Helper類暴露的方法基本一樣,僅僅除了一些特殊的要求,好比Oracle中處理bool類型的方式就和SQL Server不一樣,從而專門提供了OraBit和OraBool方法。此外,Helper類中的方法均爲static方法,以利於調用。OracleHelper的類圖例如如下:
對於數據訪問層來講,最頭疼的是SQL語句的處理。在早期的CS結構中,由於未採用三層式架構設計,數據訪問層和業務邏輯層是緊密糅合在一塊兒的,所以,SQL語句遍及與系統的每一個角落。這給程序的維護帶來極大的困難。此外,由於Oracle使用的是PL-SQL,而SQL Server和Sybase等使用的是T-SQL,二者儘管都遵循了標準SQL的語法,但在很是多細節上仍有差異,假設將SQL語句大量的使用到程序中,無疑爲可能的數據庫移植也帶來了困難。
最好的方法是採用存儲過程。這樣的方法使得程序更加整潔,此外,由於存儲過程可以以數據庫腳本的形式存在,也便於移植和改動。但這樣的方式仍然有缺陷。一是存儲過程的測試相對困難。儘管有對應的調試工具,但比起對代碼的調試而言,仍然比較複雜且不方便。二是對系統的更新帶來障礙。假設數據庫訪問是由程序完畢,在.Net平臺下,咱們僅需要在改動程序後,將又一次編譯的程序集xcopy到部署的server上就能夠。假設使用了存儲過程,出於安全的考慮,必須有專門的DBA又一次執行存儲過程的腳本,部署的方式受到了限制。
我之前在一個項目中,利用一個專門的表來存放SQL語句。如要使用相關的SQL語句,就利用keyword搜索得到相應語句。這樣的作法近似於存儲過程的調用,但卻避免了部署上的問題。然而這樣的方式卻在性能上沒法獲得保證。它僅適合於SQL語句較少的場景。只是,利用良好的設計,咱們可以爲各類業務提供不一樣的表來存放SQL語句。相同的道理,這些SQL語句也可以存放到XML文件裏,更有利於系統的擴展或改動。只是前提是,咱們需要爲它提供專門的SQL語句管理工具。
SQL
語句的使用沒法避免,怎樣更好的應用SQL語句也無定論,但有一個原則值得咱們遵照,就是「應該儘可能讓SQL語句盡存在於數據訪問層的詳細實現中」。
固然,假設應用ORM,那麼一切就變得不一樣了。因爲ORM框架已經爲數據訪問提供了主要的Select,Insert,Update和Delete操做了。好比在NHibernate中,咱們可以直接調用ISession對象的Save方法,來Insert(或者說是Create)一個數據實體對象:
public void Insert(OrderInfo order)
{
ISession s = Sessions.GetSession();
ITransaction trans = null;
try
{
trans = s.BeginTransaction();
s.Save( order);
trans.Commit();
}
finally
{
s.Close();
}
}
沒有SQL語句,也沒有那些煩人的Parameters,甚至不需要專門去考慮事務。此外,這種設計,也是與數據庫無關的,NHibernate可以經過Dialect(方言)的機制支持不一樣的數據庫。惟一要作的是,咱們需要爲OrderInfo定義hbm文件。
固然,ORM框架並非是萬能的,面對紛繁複雜的業務邏輯,它並不能全然消滅SQL語句,以及替代複雜的數據庫訪問邏輯,但它卻很是好的體現了「80/20(或90/10)法則」(也被稱爲「帕累托法則」),也就是說:花比較少(10%-20%)的力氣就可以解決大部分(80%-90%)的問題,而要解決剩下的少部分問題則需要多得多的努力。至少,那些在數據訪問層中佔領了絕大部分的CRUD操做,經過利用ORM框架,咱們就僅需要付出極少數時間和精力來解決它們了。這無疑縮短了整個項目開發的週期。
仍是回到對PetShop的討論上來。現在咱們已經有了數據實體,數據對象的抽象接口和實現,可以說有關數據庫訪問的主體就已經完畢了。留待咱們的還有兩個問題需要解決:
一、數據對象建立的管理
二、利於數據庫的移植
在PetShop中,要建立的數據對象包含Order,Product,Category,Inventory,Item。在前面的設計中,這些對象已經被抽象爲相應的接口,而事實上現則依據數據庫的不一樣而有所不一樣。也就是說,建立的對象有多種類別,而每種類別又有不一樣的實現,這是典型的抽象工廠模式的應用場景。而上面所述的兩個問題,也都可以經過抽象工廠模式來解決。標準的抽象工廠模式類圖例如如下:
好比,建立SQL Server的Order對象例如如下:
PetShopFactory factory = new SQLServerFactory();
IOrder = factory.CreateOrder();
要考慮到數據庫的可移植性,則factory必須做爲一個全局變量,並在主程序執行時被實例化。但這種設計儘管已經達到了「封裝變化」的目的,但在建立PetShopFactory對象時,仍不可避免的出現了詳細的類SQLServerFactory,也便是說,程序在這個層面上產生了與SQLServerFactory的強依賴。一旦整個系統要求支持Oracle,那麼還需要改動這行代碼爲:
PetShopFactory factory = new OracleFactory();
改動代碼的這樣的行爲顯然是不可接受的。解決的辦法是「依賴注入」。「依賴注入」的功能通常是用專門的IoC容器提供的,在Java平臺下,這樣的容器包含Spring,PicoContainer等。而在.Net平臺下,最多見的則是Spring.Net。只是,在PetShop系統中,並不需要專門的容器來實現「依賴注入」,簡單的作法仍是利用配置文件和反射功能來實現。也就是說,咱們可以在web.config文件裏,配置好詳細的Factory對象的完整的類名。然而,當咱們利用配置文件和反射功能時,詳細工廠的建立就顯得有些「多此一舉」了,咱們全然可以在配置文件裏,直接指向詳細的數據庫對象實現類,好比PetShop.SQLServerDAL.IOrder。那麼,抽象工廠模式中的相關工廠就可以簡化爲一個工廠類了,因此我將這樣的模式稱之爲「具備簡單工廠特質的抽象工廠模式」,其類圖例如如下:
DataAccess
類全然代替了前面建立的工廠類體系,它是一個sealed類,當中建立各類數據對象的方法,均爲靜態方法。之因此能用這個類達到抽象工廠的目的,是因爲配置文件和反射的運用,例如如下的代碼片段所看到的:
public sealed class DataAccess
{
// Look up the DAL implementation we should be using
private static readonly string path = ConfigurationManager.AppSettings[」WebDAL」];
private static readonly string orderPath = ConfigurationManager.AppSettings[」OrdersDAL」];
public static PetShop.IDAL.IOrder CreateOrder()
{
string className = orderPath + 「.Order」;
return (PetShop.IDAL.IOrder)Assembly.Load(orderPath).CreateInstance(className);
}
}
在PetShop中,這樣的依賴配置文件和反射建立對象的方式極其常見,包含IBLLStategy、CacheDependencyFactory等等。這些實現邏輯散佈於整個PetShop系統中,在我看來,是可以在此基礎上進行重構的。也就是說,咱們可以爲整個系統提供類似於「Service Locator」的實現:
public static class ServiceLocator
{
private static readonly string dalPath = ConfigurationManager.AppSettings[」WebDAL」];
private static readonly string orderPath = ConfigurationManager.AppSettings[」OrdersDAL」];
//……
private static readonly string orderStategyPath = ConfigurationManager.AppSettings[」OrderStrategyAssembly」];
public static object LocateDALObject(string className)
{
string fullPath = dalPath + 「.」 + className;
return Assembly.Load(dalPath).CreateInstance(fullPath);
}
public static object LocateDALOrderObject(string className)
{
string fullPath = orderPath + 「.」 + className;
return Assembly.Load(orderPath).CreateInstance(fullPath);
}
public static object LocateOrderStrategyObject(string className)
{
string fullPath = orderStategyPath + 「.」 + className;
return Assembly.Load(orderStategyPath).CreateInstance(fullPath);
}
//……
}
那麼和所謂「依賴注入」相關的代碼都可以利用ServiceLocator來完畢。好比類DataAccess就可以簡化爲:
public sealed class DataAccess
{
public static PetShop.IDAL.IOrder CreateOrder()
{
return (PetShop.IDAL.IOrder)ServiceLocator. LocateDALOrderObject(」Order」);
}
}
經過ServiceLocator,將所有與配置文件相關的namespace值統一管理起來,這有利於各類動態建立對象的管理和將來的維護