http://www.uml.org.cn/zjjs/201009141.asp
簡介
引言
服務層不直接執行任何任務。它所做的就是合理的安排一些列你提供的業務對象。服務層很清楚業務邏輯層,也很清楚領域模型。例如:你使用數據庫表模型模式的業務邏輯層,服務層會通過DataSet來進行交互。
很顯然,服務層合理的安排業務組件,同時也合理的安排應用的服務、工作流和業務邏輯的其他組件。
服務層的職責
服務層是一個額外的層,是在兩個層之間設置一個邊界。
服務層的目的是什么?
在業界有很多的應用原則都很重要,在設計軟件的時候要注意:分離關注、低耦合、高內聚。當我們討論在修改一個已有對象的時候,如何才能使得它暴露低耦合,首先想到的是增加更多的抽象。在下圖中我們看到了類Action依賴於具體類Strategy,因為Action的一個方法中引用了Strategy類。
如何降低依賴呢?而且還有一條原則:針對接口編程,不要針對實現編程。在設計的時候,應該在Action類中隱藏Strategy類。為Strategy類定義一個接口,然后定義一個工廠類來創建具體的Strategy實例,這樣Strategy的修改不再影響Action類,降低了Action類對Strategy類的依賴。
除了高級別抽象的工廠模式,服務層還有很多模式。服務層位於一對接口層之間,保持他們的低耦合和相對的獨立,但是可以很好的和他們進行通信。大多數時候,我們看到的服務層都是在表現層和業務邏輯層之間定義一個邊界。這是常見的解決方案。
合理的安排系統行為
正如你在上圖中看到的,服務層想表現層屏蔽了業務邏輯層的實現細節。但是,他不僅做了這些工作。上圖是正確的,但不是全部。
在他的核心中,每一次用戶驅動的交互都有兩個參與者:表現層實現的用戶接口,服務層實現的響應用戶行為的模塊。這也證明了服務層不僅僅是合理的安排業務邏輯,而且它也和表現層打交道。
任何來源於表現層的交互,在服務層都會找到一個響應。基於收到的輸入數據,服務層控制業務組件,包括:服務、工作流、和領域模型,當然了訪問數據庫訪問層也是必須的。
是否服務層只是直接進行數據庫操作呢?不是十分准確,業務邏輯會包括工作流和業務服務。業務邏輯應該獨立於任何數據庫細節。
服務層是表現層和其它層之間的邊界,它獲取和返回數據傳輸對象,然后將數據傳輸對象轉換為合適的領域模型類的實例。每一個通過服務層暴露的方法都整合了其他服務、工作流和通過ORM進行了數據庫操作。
什么是服務?
拋開相關的技術,服務一詞簡單的代表一層發送給另外一層的軟件請求。服務層,就是一系列服務的集合,在兩個相互通信的層中間的一個層。
什么是面向服務?
面向服務是一種將業務處理看做是一系列相互連接的服務的設計方式。面向服務不是技術本身,只是一個完成描述業務如何操作的不同的方式。
服務 vs. 類
在應用的設計中,面向服務允許你使用固定接口的獨立組件。這些組件隨時可以替換,只要保持接口的協議不發生變化,都不會影響系統。
使用服務層的好處
服務層在用戶接口和其它層之間提供了唯一的協議,使得你可以專注於應用邏輯。應用邏輯是業務邏輯的一部分,直接產生於用例。
在服務端,被執行的服務層方法,合理的安排了需要的邏輯,通過協調領域模型、特殊的應用服務、工作流。
沒有服務層,表現層就會直接調用業務邏輯層。完成一個任務,就可能需要進行多次的遠程調用。對於性能來說,這不是一件好事。
服務層實戰
引言
服務層是在交互的兩個層中間又定義了另外一個層,典型的是在表現層和業務邏輯層之間。這個中間層只是實現應用的用例的類集合。
服務和面向服務的出現,使得整個解決方案更有價值、更加成功。與表現層相比,服務層提供了松散的耦合,服務層提供商定的協議,可重用性,跨平台的部署。服務向其他類一樣,允許你調整你需要的抽象總數。
真實世界的表現層,主要是用戶前端。用戶做的每一件事都通過表現層和用戶界面。
企業級的應用,可能會有多種數據表現接口。一個接口可能就是一個用戶界面,也可能是每一個支持的平台,例如:移動應用、web、WPF、windows、Silverlight,或者是其他軟件平台。另一個接口可能是后端應用,傳入數據或者是獲取數據,並且轉變他。可能還有一個接口是一個使用應用的連接者代理-系統整合方案中的內部處理邏輯。
服務層響應來自表現層的輸入。相應的,表現層不關心另外一端的操作和模塊。重要的是模塊聲明了它能做什么。
表現層和服務層都不包含業務邏輯。表現層只知道服務層提供的粗粒度接口,服務層只知道一系列可行的相互作用的協議,處理本質的細節:事務,資源管理,協調,數據消息。
服務層在現實生活中的例子
SOA的出現,與服務層的出現一致,加強了服務層的概念,使他更吸引人。一些人爭論說在多層架構中使用SOA是有創造力的。爭論這些是毫無意義的,就好像爭論是先有雞,還是先有蛋。
在實際的使用中,使用服務層的目的,背后的理由,很多程序員和架構師還未能理解。下面,讓我們分析一個現實生活中的例子。
我們中的很多人都有做初級程序員的經歷。在某些時候,我們還會碰到一個目中無人的老板。老板可能會說:嗨,我們需要馬上為客戶定制一個系統。
你聽到了嗎?老板就是表現層。老板關心的是向經理人發送一個簡單的命令。在他的眼里,經理人暴露了一個任務和責任的列表。老板不關心經理人實際如何完成任務,但是他知道公司和經理人之間的協議中規定的經理人應該做什么(協議中也會提到,如果經理人不滿足要求,就會被替換掉)。經理人就是服務層。最終,經理人協調其他資源來完成這個任務。
如果你左右看看,在現實生活中你會找到很多服務層模式的例子。例如:孩子向父母要零花錢,編輯要求修改文稿,你從ATM中取現金,等等。
什么時候使用服務層
在任何有點復雜的應用中都應該使用服務層。如果在一個簡單的文檔管理系統,或者是一個快速建立的網站,可能只是存在幾個星期的網站中建立服務層很可能會沒有回報。
在一個分層系統中,沒有理由不使用服務層。一個可能的例外就是簡單的前端和一系列只是滿足用例的應用服務。在這種情況下,服務層很可能只是一個發報機,沒有任務組合工作。簡單的服務層還不如直接調用業務邏輯層。
相反的,在你擁有多個前端,而且是大量的應用邏輯,將應用邏輯存放在一個地方,而不是在每個應用接口中都保留副本會更加好。
服務層的優點
服務層增加抽象,解耦兩個交互的層。在任何你想獲得一個更好的系統的時候,你都應該構建一個服務層。服務層使用粗粒度的、遠程接口最小化表現層和業務層之間的通信次數。
通過服務(例如:WCF)來實現服務層的時候,你能感覺到其他的好處,例如:通過配置來改變綁定信息。
服務層的缺點
因為抽象是服務層的主要優點,對於簡單的系統可能有點過頭了。
服務層不是必須使用例如WCF這樣的服務技術。在ASP.NET中的表現層,你可以把code-behind類叫做服務層。這時候使用WCF代替普通的類,可能有點過頭了,很可能會降低性能。考慮在你的系統中使用WCF,需要考慮性能,如果性能下降的無法忍受,請選擇其他服務層技術。
服務層適用於什么樣的場景
表現層調用服務層。是一個遠程調用,還是一個本地調用?
Flower關於分布式對象設計的推薦是:不要分散你的對象。我們可以理解為:“除非是必須的,或者是有好處”。就想你所知道的,必要性和好處實際容易變化的,難以量化,但是在一些特殊的方案中,他們很容易識別。
因此,在什么場景適合服務層呢?通常來說,如果你有一個服務層,可以很容易的跨層移動,那是一件好事。在這點上,例如WCF這樣的服務技術是一個正確的工具。
如果客戶端是web網頁,服務層最好是位於本地的web服務器上。如果站點成功了,你可以將服務層分離到獨立的應用服務器,來增加擴展性。
如果客戶端是桌面應用,服務層會部署到一個獨立的物理層,並且通過遠程來訪問。這個方法類似於Software+Services的架構,客戶端除了GUI什么都沒有,全部的應用邏輯都在遠端。如果客戶端使用Silverlight,服務層會發布在Internet上,你可以建立一個完美的RIA(Rich Internet Application)應用。
實戰服務層模式
實現服務層依賴於兩個技術選擇。第一個選擇就是那什么方法或者是調用來作為服務層的基礎。使用普通的類還是服務?如果選擇服務,又該選擇哪種服務的實現技術?在windows或者是.NET平台,你的選擇比較少。你可以選擇WCF service、asp.net xml web service,或者是類似於REST之類的服務。
如果你對於.NET框架有一些了解,你應該知道創建一個wcf service或者是web service,就好像創建普通的類,然后添加一些attribute。當然,還有很多細節需要考慮,例如:web service的WSDL(web service description language)web服務描述語言,WCF的配置和數據協議。服務最終是一個包含其他內容的類。
設計服務層的類
服務層使用的類應該暴露一個協議,無論是WCF的協議還是實現接口。實現接口是一個比較好的做法,因為它更清晰的描述了一個類可以做什么,將會做什么。接口使用DTO接收和返回數據,推薦粗粒度的方法,以便它可以最小化網絡傳輸,最大化網絡吞吐量。
如何將需要的方法映射為接口和類呢?在用例的基礎上,列出一系列所需的方法,然后將他們分為邏輯組。每個組建立自己的服務或者是類。
大多數情況,你的結果是問題域的每個實體建立一個服務類,OrderService,CustomerServcie等等。這些都是應用需要的。但是,如果用戶的行為相對比較小,行為比較相似,這時候一個服務類可能就夠用了。否則,一個單一的服務類會迅速變大,會很難以維護和變化。
通常來說,我們認為沒有嚴格定義的的准則,例如:每個實體都要有自己的service,或者是一個service需要滿足用戶所有實體。服務層在系統的表現層和其他部分之間進行調節。服務層包括了粗粒度的服務(也是用例驅動的),在他們的編程接口中,實現了用例。
服務層和系統的其他部分相比,是獨立的,對於表現層來說只是一個調用內部處理流程的接口。如果用例有變化,你很可能只是修改服務層而不用修改業務邏輯。在一個相對較大的應用中,對於服務層的編程接口來說,你應該先看一下你的用例,然后使用通用的方法組織類中的方法。
實現一個服務層的類
我們推薦每個類應該實現一個接口。如果你選擇WCF,這是嚴格要求的,而且從整體上來講是一個好方法。
public interface IOrderService
{
[OperationContract]
bool Submit(BeautyCode.Entity.Order order,BeautyCode.Entity.CommunUser user,out BeautyCode.Entity.CException exception);
[OperationContract]
List<BeautyCode.Entity.Order> FindOrders(BeautyCode.Entity.OrderFind find, BeautyCode.Entity.CommunUser user,
out BeautyCode.Entity.CException exception);
[OperationContract]
BeautyCode.Entity.Order GetByOrderSeqNo(string orderSeqNo, BeautyCode.Entity.CommunUser user,
out BeautyCode.Entity.CException exception);
}
代碼
[AspNetCompatibilityRequirements (RequirementsMode=AspNetCompatibilityRequirementsMode .Allowed )]
public class OrderService:ServiceBaseImpl , IOrderService
{
private void ThrowException(BeautyCode.Entity.CommunUser user)
{
}
public bool Submit(BeautyCode.Entity.Order order, BeautyCode.Entity.CommunUser user, out BeautyCode.Entity.CException exception)
{
throw new NotImplementedException();
}
public List<BeautyCode.Entity.Order> FindOrders(BeautyCode.Entity.OrderFind find, BeautyCode.Entity.CommunUser user,
out BeautyCode.Entity.CException exception)
{
throw new NotImplementedException();
}
public BeautyCode.Entity.Order GetByOrderSeqNo(string orderSeqNo, BeautyCode.Entity.CommunUser user,
out BeautyCode.Entity.CException exception)
{
throw new NotImplementedException();
}
}
首先假設接口直接使用領域模型對象。在上面的例子中的Order類,代表我們在領域模型中建立的order實體。如果我們使用實際的領域模型對象,我們假設在業務邏輯中使用領域模型的模式。如果你使用數據表模型的模式,上面代碼中的order類應該替換為DataTable。我們一會在回到DTO的討論上來。
submit方法需要整合應用內部的服務,檢查用戶的賬戶狀態,檢查訂單中商品的有效性,同步廠商的商品信息。submit方法是一個典型的服務層方法,它對表現層提供了單一的協議,與不同的領域模型和業務邏輯進行多個步驟的操作。
FindOrders方法返回一個order集合,GetByOrderSeqNo返回一個特定的order。此外,假定我們在業務邏輯層使用領域模型的模式,沒有專門的數據傳輸對象。很多的架構師推薦在服務層不出現Create、Read、Update、Delete(CRUD)方法。FindOrders和GetByOrderSeqNo方法本質上來說就是CRUD中的Read方法。
而且,方法依賴於你的用例。如果用例中有用戶點擊一個地方顯示訂單列表,或者是單個訂單的詳細信息的需要,那么這些方法就必須要有。
處理角色和安全
FindOrders方法應該只是返回當前用戶可以看到的訂單。
如果你把安全當回事來考慮的話,就應該在服務層的每一個方法中檢查調用者的身份,對未授權的用戶拒絕方法的調用。
如果你不想在每個方法中重復驗證用戶的身份,那就需要在服務層的方法上面添加attribute來實現身份驗證。
服務層起到一個看門人的作用,通常不需要將role信息傳輸到業務邏輯層中去驗證,除非有好的理由。但是,如果有這么一個好的理由,使得你不得不將role信息傳到業務邏輯層中,也是不錯的。
服務層的相關模式
1 引言
我們把服務層看做是暴露給用戶界面的一個服務集合。大多數時候,我們會發現服務層的方法很容易滿足用戶的行為。在大多數企業應用中,CRUD是常用的操作。有的時候在一次操作中會處理多個實體。
服務層包括角色管理,數據驗證,通知,調整返回給用戶界面的數據,或者是整合系統可能的需求。
在談到這些的時候,一些設計模式可能會有幫助。下面是一些在實現服務層的過程中有幫助的模式。
2 遠程外觀模式Remote Facade Pattern
遠程外觀模式是用來修改已經實現的方法的粒度的,外觀模式沒有實現任何新功能。它只是在原有的API之上包裝一層不同的外觀。為什么會需要外觀模式呢?
2.1 使用外觀模式的動機
外觀模式改變一個已經存在的對象的訪問方式。舉一個貨運商在線服務的例子。每一個貨運商對於注冊運輸的貨物、跟蹤貨物和其他服務都有自己的API,他們的細節在你的應用中可能都用不着。通過建立一個統一的API,你可以隱藏貨運商API的細節,使得你的應用有一個清晰的接口。
換句話說,如果你想要你的接口處理一個簡單的接口,這些必要性強迫你創建另外一個外觀。實際上,這就是經典的遠程外觀模式的精確定義。
外觀模式的好處就是允許你在一系列定義好的細粒度對象之上,定義粗粒度的接口。
面向對象使得你創建了很多小的對象,職責分離,細粒度對象。但是這些對象不適合分布式。為了是這些對象有效,你需要做出一些調整。你不想改變細粒度對象的任何實現,但是你需要通過這些對象可以執行一批操作。在這種情況下,你需要創建新的方法,移動更大的數據。當你這么做的時候,實際上就是修改了已經存在的接口粒度,也就是創建了外觀模式。讓我們回到貨運訂單的例子。通過你自己的API你可以發送多個訂單,不用使用貨運商的API來發送單個的訂單,這里我們假設貨運商不支持多個訂單的API。
2.2 遠程外觀模式和服務層
經過設計,服務層本質上擁有了粗粒度的接口,因為它更趨向於抽象一定數量的細小操作,來給客戶端使用。在這點上,服務層已經是一種位於業務邏輯層和領域層對象之上的外觀模式。
{
void Create(Order o);
List<Order> FindAll();
Order FindByID(int orderID);
}
如果你使用WCF,需要在服務層接口上添加協議attribute。
public interface IOrderService
{
[OperationContract]
void Create(Order o);
[OperationContract]
List<Order> FindAll();
[OperationContract]
Order FindByID(int orderID);
}
所有非基本類型的數據都需要添加datacontract標記。在WCF運行的時候,這些標記會為指定的類型自動創建數據傳輸對象DTO。
public class Order
{
[DataMember]
int OrderID {get; set;};
[DataMember]
DateTime OrderDate {get; set;};
}
如果你使用asp.net xml web service。上面的類不要添加任何標記,只需要在服務層類的方法上添加WebMethod標記就可以了。
在使用外觀模式的時候,你可能會需要更粗粒度的接口,你也可能會用到DTO,或者你都會用到。下面的接口就和領域對象解耦了,更加聚焦在和表現層的交互上。
{
void Create(OrderDto o);
List<OrderDto> FindAll();
OrderDto FindByID(int orderID);
List<OrderDto> SearchOrder(QueryByExample query);
}
OrderDto可能是領域類Order的子集或者是包含Order(可能會整合依賴的對象OrderDetail),QueryByExample是一個專門的類,會傳輸用戶界面的一些條件給服務層,我更喜歡定義寫Find類,例如OrderFind。里面就是一些查詢條件,例如:時間、編號、金額等等。
實際上,如果用戶界面改動比較大的話,就需要修改服務層。你可能會需要重構服務層,或者是在外面再次的使用外觀模式來包裝。
3 數據傳輸對象模式
DTO(Data Transfer Object數據傳輸對象)僅僅是一個跨越應用邊界傳輸數據的對象,主要的目的是最小化網絡的往返。
3.1 使用DTO模式的動機
有兩個主要的動機。一個是在你調用遠程對象的時候,最小化網絡的往返;另外一個是在前端顯示和后端的領域模型之間維護一個松散的耦合關系。
在多數情況,DTO都只有屬性,沒有操作。DTO在領域模型方案中扮演着重要的角色。不是在所有的情況,表現層都可以直接使用本地的領域對象。如果是服務層和表現層在同一個物理層的話,例如表現層是web網頁的形式,可以直接使用。如果服務層和表現層不在同一個物理層,最好不要通過領域對象來交換數據。主要的原因是你的領域對象之間可能是彼此依賴的,甚至是循環引用的關系,這會嚴重的影響它們的序列化能力,甚至是序列化失敗的問題。
關於為什么需要DTO以及什么時候需要DTO的個人理解,僅供參考:
都在同一層的話,不存在對象通過網絡傳輸的問題,所以可以直接使用領域模型,而且效率還高,沒有網絡傳輸和延遲。
不在同一層的話,就存在網絡傳輸的問題,領域模型中既包含了數據,也包含了操作,數據可以在層之間傳輸,可是操作時不能傳輸的。而且為了解耦領域模型中的對象和傳輸的對象,而且領域模型的粒度較小,傳輸領域模型的話,可能需要多次調用,才可以滿足一次界面的顯示,這樣會增加網絡的往返次數。
同時,界面顯示的內容可能來自幾個領域模型對象,也可能是一個領域模型對象中的幾個屬性。所以才單獨建立DTO用來在層之間傳輸數據。
獨立出來的話,就可能就會涉及到序列化的問題。
例如,要考慮到WCF和asp.net xml web service的xml序列化都不能處理循環引用的情況。同時,如果你使用領域模型,你會發現每一個Customer對象都有多個Order對象,每一個Order對象都會對應一個Customer對象。在復雜的領域模型中,循環引用很常見。
循環引用:
循環引用就是兩個對象相互引用,每個對象的都有對方類型的屬性存在,這樣就造成了循環引用。也就是下面的情況。
代碼
public class Order
{
public string OrderSeqNo
{
get;
set;
}
public Customer Customer
{
get;
set;
}
}
{
public List<Order> Orders
{
get;
set;
}
}
DTO可以幫助你避免這種風險,使你的系統更加整潔。但是,它也引入和新的復雜等級。我們需要額外的層,DTO適配層。
3.2 數據傳輸對象和服務層
當你在開始的架構會議中討論DTO的時候,你經常會聽到反對使用DTO的聲音。數據傳輸對象可能會浪費開發時間和資源。問題是DTO的存在有他的必要性。可以不使用它們,但是他們在企業級架構中任然扮演重要的角色。
在理論上,我們提倡在兩個層之間發生通信的時候使用DTO,包括表現層和服務層的通信。另外,我們還提倡在每一個截然不同的用戶界面使用不同的DTO,甚至是不同的DTO請求和相應。
在實際的使用中,事情可能有所不同。DTO意味着在服務層添加新的一層代碼,隨着而來的是復雜性。只要沒有更好的選擇,這樣做是可以接受的。我們在強調DTO的花銷的時候很容易低估它的花銷。當你擁有上百個領域對象的時候,2-3倍的類就會使噩夢了。
只在使用DTO的好處很明顯,而且必要性很明顯的時候再用DTO,否則就直接使用領域對象。
使用DTO我們還需要內部的、項目定制的ORM層。
對於ORM層,我們有很多工具可以選擇,商業的和開源的。WCF和asp.net xml web service在序列化數據的時候生成DTO,但是對於數據格式的控制,它們提供的功能有限。通過 WCF中的[IgnoreDataMember]和asp.net xml web service中的[XmlIgnore ],你可以將一些屬性從DTO中刪除,也就是在DTO對象中沒有這些屬性。但是在請求和相應需要不同的類的時候,或者是不同的用戶界面使用不同的DTO的時候,你沒有自動產生特定的DTO的方法。到目前為止,還不存在這樣的向導。需要我們手動來完成,自己定義DTO。
4 適配器模式
當你在分層系統中使用DTO的時候,你可能會為不同的接口而調整領域模型。你需要實現適配器模式,一個經典而且很流行的模式。適配器的本質是將一個類的接口轉換為用戶希望的另外一個接口。
4.1 使用適配器模式的動機
適配器的職責是將數據表現為另外一種格式。例如:適配器會將從數據庫中讀取的bit格式的列,轉換為用戶接口中的boolean,來方便使用。
在一個分層的方案中,適配器模式被用於將一個領域對象轉換為DTO對象,或者是反過來。在適配器類中沒有復雜的邏輯,但是你需要一些適配器類來賦值DTO對象。
4.2 適配器模式和服務層
給每一個DTO配置一個適配器類,必然會增加開發的成本。
在評估DTO的時候,對於將要產生的一大丟類,你應該持續的考慮維護他們的問題。你要記住,在每一個適配器類中, 需要有兩個功能,一個是從領域對象到DTO;一個是從DTO到領域對象。