目錄html
本文將介紹領域驅動設計(DDD)戰術模式中另外一個很是重要的概念 - 領域服務。在前面兩篇博文中,咱們已經學習到了什麼是值對象和實體,而且可以比較清晰的定位它們自身的行爲。可是在某些時候,你會發現某一些業務行爲好像不容易落到單個實體或者值對象身上,而且會爲放置這一部分業務邏輯而困惑。此時,你可能須要一個領域服務來完成操做。git
那麼,到底什麼是領域服務呢?怎麼發現領域中的領域服務呢?領域服務和傳統的應用服務又有什麼區別呢?本文將從不一樣的角度來帶你們從新認識一下「領域服務」這個概念,而且給出相應的代碼片斷(本教程的代碼片斷都使用的是C#,後期的實戰項目也是基於 DotNet Core 平臺)。github
在開始以前,仍是說一點題外話吧:若是你們讀過這個系列的前幾篇文章,可能都會發現該系列的風格都是從原著的解析開始,而後結合了自身的一些案例和實際場景來爲你們解讀領域驅動中的一些概念。我也不知道這樣的寫做方式能不能讓你們更清楚的理解,因此若是你們有什麼建議的話能夠在評論區留言,我必定會認真的聽取你們的意見和建議。c#
在文章中,我會盡量避免各種名稱的簡寫(好比事件溯源,有些同窗喜歡簡寫爲ES),雖然簡寫有時候確實會很方便,可是會讓人與人之間的溝通成本無形的增大,因此在個人博文中只要能不用簡寫的地方我都不會使用簡寫。架構
另外還有一點就是,可能前期屬於概念性的東西比較多,因此就沒有現成的github代碼供你們參考,很少你們不用擔憂,在完成這幾回的概念學習以後咱們就開始咱們的code time(●'◡'●)。框架
回到正題吧,什麼是領域服務呢?看看原著原著《領域驅動設計:軟件核心複雜性應對之道》中所說起到的領域服務的概念:性能
在某些狀況下,最清楚、最實用的設計會包含一些特殊的操做,這些操做從概念上講不屬於任何對象。與其把它們強制地歸於哪一類,不如順其天然地在模型中引入一種新的元素,這就是Service(服務)。
當領域中的某個要的過程或轉換操做不屬於實體或值對象的天然職責時,應該在模型中添加一個做爲獨立接口的操做,並將其聲明爲Service.定義接口時要使用模型語言,並確保操做名稱是UBIQUITOUS LANGUAGE中的術語。此外,應該將Service定義爲無狀態的。學習
額。。。。「李姐萬歲」。這個概念很差理解的緣由是由於:首先它假設咱們尋找到了領域中一些「包含特殊的操做」,也就是說咱們在此時已經具有了劃分領域中各類對象以及其對應行爲的能力,而後咱們再來考慮提取出這個傳說中的「service」(也就是咱們本次的主題領域服務)。而每每現實則是,做爲一個初學者,咱們並不能合理的抽象出各個對象,而且也沒有一個好的案例來進行體驗性的思考。因此在讀這個概念的時候就很迷惑,咱們沒法找到概念中的「這些操做」是什麼東西,也就更不能理解這個Service是什麼了。設計
「在本身的私人飛機裏面玩兒電子遊戲是什麼感受呢? 呃.....好像前提是我得有錢買一架飛機吧?」
我思考了不少種方法來表述「領域服務」,可是想了半天好像都不太容易能讓人理解。因此該篇博文采用先從案例入手的思路,但願你們能從這個案例可以理解出領域服務的用處。3d
來回顧一下上一篇文章 《如何運用DDD - 實體》 中咱們所提煉出來的一個實體對象:
public class Itinerary { public int ID { get; set; } public List<Person> Participants { get; set; } public List<Address> Places { get; set; } public ItineraryNote Note { get; set; } public ItineraryTime TripTime { get; set; } public ItineraryStatus Status { get; set; } //ctor public void ChangeNote(string content) { Note = new ItineraryNote(content); } }
該實體對象代表了一次旅行的行程。目前做爲示例,咱們僅僅知道了在該領域中咱們容許修改行程的備註信息,因此咱們在上一篇文章中爲它賦予了修改備註的一個行爲。
根據項目的進展,咱們如今捕獲到了另外一個需求:若是行程沒有結束,用戶訪問到該行程,系統會根據用戶目前所在的地點爲用戶推薦附近好吃的美食。
這是一個很是人性化以及好用的功能,也是該產品能夠和其餘同類型的產品系統競爭的優點。因此咱們理應將它放置於領域來考慮。從該功能需求的描述來看,咱們要作的是一個推薦美食的行爲。可是讓咱們矛盾的是,推薦美食這一個動做,咱們應該將它歸屬於誰呢? 給旅程?讓旅程實體來推薦美食? 很顯然,你並不會這麼作。旅程僅僅關心的是本次旅行的基本信息,地點人物時間等,咱們不會將推薦美食這一個動做給它,讓它成爲一個萬能的機器。
來回顧一下上面所說的概念:「在某些狀況下,最清楚、最實用的設計會包含一些特殊的操做,這些操做從概念上講不屬於任何對象。」 仔細讀幾遍,納尼?這不是說的就是這個狀況嗎? 在如今這個狀況下,咱們出現了一個推薦美食的操做,可是它卻不屬於任何對象。
當走到這一步時,可能咱們已經有一點理解領域服務了。接下來,繼續往下走。如今,咱們已經明白了,可能咱們須要一個Service來處理這一個操做。嘗試着來創建一個 RecommendFoodsService 。
public class RecommendFoodsService { public List<RecommendFoodInfo> RecommendFoods(Itinerary currentItinerary) { //todo } }
在該領域服務中,有一個RecommedFoods的方法,它經過獲取到當前的旅程,返回一個推薦美食的列表。它內部的實現方法多是這樣的:(在這裏咱們假設Itinerary的Places中的最後一個地點就是咱們的當前地點,並且咱們已經有一個叫作餐廳 Restaurant 的實體,該實體提供了有關餐館的一系列信息和行爲。固然,你能夠本身嘗試創建餐廳這樣一個實體,以便加深對實體章節的印象)
public class RecommendFoodsService { public List<FoodInfo> RecommendFoods(Itinerary currentItinerary) { var recommendFoods = new List<FoodInfo>(); //Get Last Address int lastCountIndex = currentItinerary.Places.Count -1; var currentAddress = currentItinerary.Places[lastCountIndex]; var nearbyRestaurants = Restaurants.Where(s=> s.Address.isNearby(currentAddress)).ToList(); foreach(var restaurant in nearbyRestaurants) { var food = restaurant.GetRankNoOneFood(); recommendFoods.Add(new FoodInfo(food,restaurant.Address)); } return recommendFoods; } }
OK,到目前咱們已經完成了一個演示版本的領域服務,在該服務中,咱們經過獲取到當前的旅程的位置,根據該位置,從系統中存在的餐館集合中找到了距離該位置最近的餐廳,而後再將這些餐廳中排名評價最好的一道菜推薦給用戶。
來看看上面的行爲中出現了哪些東西,首先是咱們的行程,而後是餐館。經過合理的處理這兩個實體之間的關係,咱們完成了咱們的一系列操做,而且返回了一個美食信息的集合(在這裏美食信息咱們定義爲了一個值對象)。要注意,雖然咱們裏面包含了幾個實體和幾個值對象,以及使用了他們之間的不一樣行爲,可是從推薦美食這一個行爲來看,他們其實是一個總體,是密不可分的處理邏輯(敲重點!!!)。
上面的版本咱們將他做爲一個演示版原本定義,是由於在實際的狀況中,咱們每每是經過存儲庫(Repository,有關該內容的介紹會在後期文章中介紹)來獲取到實體集合的信息的,就如同上面代碼中的Restaurants。有可能更貼近於咱們現實中的代碼是相似於下面這樣,不過咱們如今能夠不用考慮這種寫法,由於裏面涉及到了存儲庫(倉儲 Repository) 和 聚合根(AggregateRoot) 的概念,而如今咱們只須要理解好領域服務就行了。
public List<FoodInfo> RecommendFoods(int currentItineraryID) { var recommendFoods = new List<FoodInfo>(); //Get Last Address var currentItinerary = itineraryRepository.Get(currentItineraryID); int lastCountIndex = currentItinerary.Places.Count -1; var currentAddress = currentItinerary.Places[lastCountIndex]; var nearbyRestaurants = restaurantRepository.GetNearbyRestaurant(currentAddress); foreach(var restaurant in nearbyRestaurants) { var food = restaurant.GetRankNoOneFood(); recommendFoods.Add(new FoodInfo(food,restaurant.Address)); } return recommendFoods; }
來吧,根據咱們如今所理解和發現的內容,來看一下領域服務的一些特色:
若是你在進行下面的操做時,可能證實你須要一個領域服務:
(ps: A,B,C指的是領域對象中的值對象或者實體)
其實在使用領域驅動中,還有一個服務叫作應用服務,應用服務是劃分在應用層的服務。而每每都是由於叫作服務,因此你們很難區分它與領域服務有什麼區別,最終的結果就是要麼形成應用服務很龐大(全部的邏輯編排都在該層處理了),要麼就是應用服務很薄弱(就一句調用領域服務的代碼)。無獨有偶,當應用服務開始混亂時,領域服務也會變得混亂,由於原有領域服務的邏輯你可能給了應用服務,而應用服務的邏輯又給了領域服務。
在比較二者以前,來看一看傳統領域驅動設計爲你們提供的四層架構示意圖:
從圖中能夠看到,應用層保持了對領域層的引用關係,也就是說在應用層中,能夠訪問到領域對象。因此讓應用層也具有了編排領域對象的能力。這一點和咱們的領域對象的特徵相同了,因此在不少時候,你們對應用服務和領域服務的區分難度就加大了。
關於應用服務,由於在原著中我沒有找到對應的關鍵語句,因此選取了網上的一些結論供你們參考:
應用服務是用來表達用例和用戶故事(User Story)的主要手段。
應用層經過應用服務接口來暴露系統的所有功能。在應用服務的實現中,它負責編排和轉發,它將要實現的功能委託給一個或多個領域對象來實現,它自己只負責處理業務用例的執行順序以及結果的拼裝.
從上面的結論中咱們大概能夠知道,應用服務是爲了讓應用可以運用而且支撐對外的用戶可以訪問領域對象和執行領域邏輯的一層。就比如在dotnetoore中,用戶能夠經過訪問咱們定義的controller來訪問咱們的業務對象,而且還能夠經過controller暴露出來的接口來執行業務邏輯。
所以,咱們能夠將應用服務考慮爲執行業務邏輯的一箇中介(可能這樣定義也不太好),它沒有涉及到核心領域的任何邏輯過程,它只負責了一些的驗證,構件的支持等(好比日誌,性能監控等)。
在上面識別領域服務中,咱們已經捕獲到了這樣一個需求:「若是行程沒有結束,用戶訪問到該行程,系統會根據用戶目前所在的地點爲用戶推薦附近好吃的美食。」 後來需求又增長了一項:「咱們能夠用短信的方式將美食通知給客戶。」
那麼考慮這樣一個需求,咱們該把短信通知這一個功能實現放在哪兒呢?或者說將發短信這個行爲操做放在哪兒呢?咱們來考慮一下將他放置在領域服務中:
public class RecommendFoodsService { public List<FoodInfo> RecommendFoods(Itinerary currentItinerary) { var recommendFoods = new List<FoodInfo>(); //Get Last Address int lastCountIndex = currentItinerary.Places.Count -1; var currentAddress = currentItinerary.Places[lastCountIndex]; var nearbyRestaurants = Restaurants.Where(s=> s.Address.isNearby(currentAddress)).ToList(); foreach(var restaurant in nearbyRestaurants) { var food = restaurant.GetRankNoOneFood(); recommendFoods.Add(new FoodInfo(food,restaurant.Address)); } //在這裏添加短信發送? SmsUtil.Send(currentItinerary.Participants,recommendFoods); return recommendFoods; } }
咱們在原有代碼的基礎上,添加了一行代碼,爲其實現短信通知功能,如今這樣已經符合咱們的需求了。可是!!!!將短信通知放置在這裏好嗎?爲解開這個問題,咱們須要考慮:「短信發送是我領域提煉出來的行爲嗎?」,「若是沒有這個行爲,對業務邏輯有什麼影響?」
來想想,發短信是領域提煉出來的嗎? 咱們一直都在關心有關旅程的問題,很顯然旅程中的各類纔是咱們主要關心的對象。那麼發短信就不是咱們所提煉出來的東西,它只是須要咱們附帶的支持功能罷了。
那麼若是沒有這個行爲,對業務邏輯有什麼影響呢? 它會不會影響我完成美食推薦這個行爲? 很顯然,不會! 還記得咱們在上文說的一個領域服務的特色嗎:領域服務中的操做,從領域的角度來看,它是一個總體。 若是總體中的一部分喪失它就不能完成業務了。那麼在如今這個推薦美食的業務中,若是把餐廳的一部分拿掉會是什麼樣子呢?OMG,這個服務已經廢了,它失去了已有的功能。那若是把短信發送拿掉呢?好像沒有一點點影響。
那麼這個短信發送,到底放在哪兒呢? 應用服務!!!!!
public class ItineraryApplicationService { public string RecommendFoods(int currentItineraryID) { Logger.Log("執行推薦美食業務"); var participants = itineraryRepository.Getparticipants(currentItineraryID); var foods = RecommendFoodsService.RecommendFoods(currentItineraryID); SmsUtil.Send(foods); return foods.toJson(); } }
咱們在應用層定義了一個叫作ItineraryApplicationService的應用服務,它對外提供了一個RecommendFoods的接口,客戶端(App,網頁等)能夠透過該API來完成推薦美食這一系列的操做。推薦美食的行爲咱們已經封裝在了領域服務中,應用服務根本不須要知道內部的邏輯就能夠完成操做,這也驗證了咱們上面說的一點:從領域的角度看,領域服務是一個總體。
就通常的應用來講,認證受權是應用服務。爲何呢?由於它每每只是給你提供了維持系統容許的基礎功能,而並不是你領域執行的必須。也許,這還很差理解,那麼咱們就來嘗試一下將它定義爲領域服務來看一看。考慮改爲那個發短信的例子,咱們實現了一個錯誤版本的領域服務,那麼如今咱們把領域服務的發短信替換爲身份驗證代碼,而後放置在方法塊最前面。來吧?繼續回答上面的問題,他們是一個總體嗎?若是剝離了這個代碼,對行爲有什麼影響? 慢慢的你就會將它從領域服務中拿出來。
可是假如你正在實現一個組織權限軟件,它可能會被定義在領域之中。由於你的領域就是認證的一系列操做,你須要認真的去思考它,一旦失去了認證的代碼可能你的應用就沒法提供正常的功能。
你己經和領域專家談論過涉及多個實體的領域概念了,但你不肯定哪一個實體「擁有」行爲。看起來該行爲並不屬於任何一個實體,但當你嘗試將該行爲強制適配到實體中的任何一個時,處理起來就會有點棘手了。這一思惟模式就是須要領域服務的強烈跡象。[噓,這句話是我copy的。(*^__^*) ]
是否是隻有領域服務才能調度值對象和實體等領域對象呢? 固然不是,應用服務也能夠。
這也是一個你們常見的問題:將全部實體、值對象、倉儲都經過領域服務來編排完成業務邏輯。從而使得應用服務層很是的薄,每每只有一行調用領域服務的代碼(日誌,性能等代碼經過一些現有框架自動完成)。
嘗試將部分調度權限分配給應用服務,它不會影響你的領域代碼可讀性,反而會使得閱讀更加清晰。當你發現你的邏輯編排只是調用實體或值對象之間的行爲,而沒有構成一個完整的領域業務行爲的時候(好比有一個Api表示了獲取一次旅行地點距離的功能,你能夠不用將該功能考慮爲領域服務,在應用服務中經過傳入的ID,在倉儲中獲取本次旅行的行程地址,而後交給系統中的距離轉換功能計算出距離,而後返回給客戶端),請考慮將它設置爲應用服務。
爲何會這樣說呢?若是你發如今你創建的領域模型中,實體和值對象的行爲只是零星一點,而實體和值對象實現行爲操做的動做都是經過領域服務來完成的。那麼,你也許用錯了領域服務,去從新認識你所識別出的實體和值對象,爲它們賦予他們自身的行爲,刪除這些錯誤的領域服務。
本次咱們介紹了領域驅動設計戰術模式中的領域服務。同時也對比了領域服務和應用服務,該部份內容可能介紹的還不是太完整,但願你們能從例子中理解二者之間的差別,後期若是有時間的話會爲你們寫一篇博文專門來區別領域服務和應用服務。在講解的過程當中,咱們還涉及到了一切戰術模式中的其餘概念,好比Repository和AggregateRoot,這兩個概念將在後期的文章中爲你們帶來介紹。
強烈給你們推薦如今正在上映的一部動漫電影 《若能與你共乘海浪之上》。喜歡動漫的同窗可不要錯過哦。