如何運用領域驅動設計 - 領域服務


概述

本文將介紹領域驅動設計(DDD)戰術模式中另一個非常重要的概念 - 領域服務。在前面兩篇博文中,我們已經學習到了什么是值對象和實體,並且能夠比較清晰的定位它們自身的行為。但是在某些時候,你會發現某一些業務行為好像不容易落到單個實體或者值對象身上,並且會為放置這一部分業務邏輯而困惑。此時,你可能需要一個領域服務來完成操作。

那么,到底什么是領域服務呢?怎么發現領域中的領域服務呢?領域服務和傳統的應用服務又有什么區別呢?本文將從不同的角度來帶大家重新認識一下“領域服務”這個概念,並且給出相應的代碼片段(本教程的代碼片段都使用的是C#,后期的實戰項目也是基於 DotNet Core 平台)。

什么是領域服務

在開始之前,還是說一點題外話吧:如果大家讀過這個系列的前幾篇文章,可能都會發現該系列的風格都是從原著的解析開始,然后結合了自身的一些案例和實際場景來為大家解讀領域驅動中的一些概念。我也不知道這樣的寫作方式能不能讓大家更清楚的理解,所以如果大家有什么建議的話可以在評論區留言,我一定會認真的聽取大家的意見和建議。

在文章中,我會盡可能避免各類名稱的簡寫(比如事件溯源,有些同學喜歡簡寫為ES),雖然簡寫有時候確實會很方便,但是會讓人與人之間的溝通成本無形的增大,所以在我的博文中只要能不用簡寫的地方我都不會使用簡寫。

另外還有一點就是,可能前期屬於概念性的東西比較多,所以就沒有現成的github代碼供大家參考,不多大家不用擔心,在完成這幾次的概念學習之后我們就開始我們的code time(●'◡'●)。

回到正題吧,什么是領域服務呢?看看原著原著《領域驅動設計:軟件核心復雜性應對之道》中所提及到的領域服務的概念:

在某些情況下,最清楚、最實用的設計會包含一些特殊的操作,這些操作從概念上講不屬於任何對象。與其把它們強制地歸於哪一類,不如順其自然地在模型中引入一種新的元素,這就是Service(服務)。
當領域中的某個要的過程或轉換操作不屬於實體或值對象的自然職責時,應該在模型中添加一個作為獨立接口的操作,並將其聲明為Service.定義接口時要使用模型語言,並確保操作名稱是UBIQUITOUS LANGUAGE中的術語。此外,應該將Service定義為無狀態的。

李姐萬歲

額。。。。“李姐萬歲”。這個概念不好理解的原因是因為:首先它假設我們尋找到了領域中一些“包含特殊的操作”,也就是說我們在此時已經具備了划分領域中各種對象以及其對應行為的能力,然后我們再來考慮提取出這個傳說中的“service”(也就是我們本次的主題領域服務)。而往往現實則是,作為一個初學者,我們並不能合理的抽象出各個對象,並且也沒有一個好的案例來進行體驗性的思考。所以在讀這個概念的時候就很迷惑,我們無法找到概念中的“這些操作”是什么東西,也就更不能理解這個Service是什么了。

“在自己的私人飛機里面玩兒電子游戲是什么感覺呢?   呃.....好像前提是我得有錢買一架飛機吧?”

從實際場景下手

我思考了很多種方法來表述“領域服務”,但是想了半天好像都不太容易能讓人理解。所以該篇博文采用先從案例入手的思路,希望大家能從這個案例能夠理解出領域服務的用處。

來回顧一下上一篇文章 《如何運用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的方法,它通過獲取到當前的旅程,返回一個推薦美食的列表。它內部的實現方法可能是這樣的:(在這里我們假設ItineraryPlaces中的最后一個地點就是我們的當前地點,而且我們已經有一個叫做餐廳 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;
    }

來吧,根據我們現在所理解和發現的內容,來看一下領域服務的一些特點:

  • 領域服務處理的是領域中的對象,比如實體、值對象等
  • 領域服務是負責對領域中一系列對象的編排處理
  • 當我們發現一個操作無法賦予一個實體或者值對象,且該操作又對業務流程很重要時,我們往往需要使用領域服務
  • 領域服務中的操作,從領域的角度來看,它是一個整體

如果你在進行下面的操作時,可能證明你需要一個領域服務:

  • 通過A和B,得到一個C。
  • A需要一個繁瑣的內部策略才能得到一個結果B。

(ps: A,B,C指的是領域對象中的值對象或者實體)

領域服務VS應用服務

其實在使用領域驅動中,還有一個服務叫做應用服務,應用服務是划分在應用層的服務。而往往都是因為叫做服務,所以大家很難區分它與領域服務有什么區別,最終的結果就是要么造成應用服務很龐大(所有的邏輯編排都在該層處理了),要么就是應用服務很薄弱(就一句調用領域服務的代碼)。無獨有偶,當應用服務開始混亂時,領域服務也會變得混亂,因為原有領域服務的邏輯你可能給了應用服務,而應用服務的邏輯又給了領域服務。

在比較兩者之前,來看一看傳統領域驅動設計為大家提供的四層架構示意圖:

DDD四層

從圖中可以看到,應用層保持了對領域層的引用關系,也就是說在應用層中,可以訪問到領域對象。所以讓應用層也具備了編排領域對象的能力。這一點和我們的領域對象的特征相同了,所以在很多時候,大家對應用服務和領域服務的區分難度就加大了。

關於應用服務,因為在原著中我沒有找到對應的關鍵語句,所以選取了網上的一些結論供大家參考:

應用服務是用來表達用例和用戶故事(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,這兩個概念將在后期的文章中為大家帶來介紹。

  • 15樓的評論中 @todo 朋友給了一個很好的建議,是當時寫作的時候忽略的問題。

 
 
 

小彩蛋

強烈給大家推薦現在正在上映的一部動漫電影 《若能與你共乘海浪之上》。喜歡動漫的同學可不要錯過哦。
《若能與你共乘海浪之上》


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM