如何運用DDD - 領域服務

来源:https://www.cnblogs.com/uoyo/archive/2019/12/11/12023896.html
-Advertisement-
Play Games

如何運用DDD 領域服務 [toc] 概述 本文將介紹領域驅動設計(DDD)戰術模式中另一個非常重要的概念 領域服務。在前面兩篇博文中,我們已經學習到了什麼是值對象和實體,並且能夠比較清晰的定位它們自身的行為。但是在某些時候,你會發現某一些業務行為好像不容易落到單個實體或者值對象身上,並且會為放置這 ...


目錄

如何運用DDD - 領域服務

概述

本文將介紹領域驅動設計(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,這兩個概念將在後期的文章中為大家帶來介紹。

小彩蛋

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


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 在JAVA中,解析有三種方式: Dom解析(支持改刪,耗記憶體)、 Sax解析(不支持改刪,不耗記憶體)、 Pull解析(在Android中推薦使用的一種解析XML的方式,在下章學習)、 1.支持Dom與Sax解析的開發包 分為兩種. JAXP: 由sun公司推出的解析標準實現(本章只學習該包的解析方法 ...
  • 簡介 SpringMvc文件上傳的實現,是由commons-fileupload這個jar包實現的。 需求 在修改商品頁面,添加上傳商品圖片功能。 Maven依賴包 pom.xml <!-- 文件上傳 --> <dependency> <groupId>commons-fileupload</gro ...
  • 前言本文的文字及圖片來源於網路,僅供學習、交流使用,不具有任何商業用途,版權歸原作者所有,如有問題請及時聯繫我們以作處理。作者:OSinooO 本人屬於python新手,剛學習的 python爬蟲基礎迫不及待地想試一試,看了論壇里大佬們寫的線上翻譯爬蟲程式,想著自己把它寫出來,以下是我爬微軟翻譯的過 ...
  • @ "TOC" 演算法題訓練網站: "http://www.dotcpp.com" 1.簡單的a+b (1)題目地址: "https://www.dotcpp.com/oj/problem1000.html" (2)演算法解析: 首先要能夠接收到橫向用空格分開的數據,並知道當運行的時候,在什麼地方可以停 ...
  • requests對象的get和post方法都會返回一個Response對象,這個對象裡面存的是伺服器返回的所有信息,包括響應頭,響應狀態碼等。其中返回的網頁部分會存在.content和.text兩個對象中。 兩者區別在於,content中間存的是位元組碼,而text中存的是Beautifulsoup根 ...
  • 不少剛學習.net core朋友對中間件的概念一直分不清楚,到底StartUp下的Configure方法是在做什麼? public void Configure(IApplicationBuilder app, IHostingEnvironment env) { if (env.IsDevelop ...
  • 獲取足球的3D坐標後,在每一個坐標位置創建一個ModelVisual3D元素,既能實現炫酷的3D界面。同樣根據這些點也能獲取到足球的每一個面。 ...
  • netcoer 創建騰訊雲私有鏡像 發佈到docker 實戰 ...
一周排行
    -Advertisement-
    Play Games
  • 概述:在C#中,++i和i++都是自增運算符,其中++i先增加值再返回,而i++先返回值再增加。應用場景根據需求選擇,首碼適合先增後用,尾碼適合先用後增。詳細示例提供清晰的代碼演示這兩者的操作時機和實際應用。 在C#中,++i 和 i++ 都是自增運算符,但它們在操作上有細微的差異,主要體現在操作的 ...
  • 上次發佈了:Taurus.MVC 性能壓力測試(ap 壓測 和 linux 下wrk 壓測):.NET Core 版本,今天計劃準備壓測一下 .NET 版本,來測試並記錄一下 Taurus.MVC 框架在 .NET 版本的性能,以便後續持續優化改進。 為了方便對比,本文章的電腦環境和測試思路,儘量和... ...
  • .NET WebAPI作為一種構建RESTful服務的強大工具,為開發者提供了便捷的方式來定義、處理HTTP請求並返迴響應。在設計API介面時,正確地接收和解析客戶端發送的數據至關重要。.NET WebAPI提供了一系列特性,如[FromRoute]、[FromQuery]和[FromBody],用 ...
  • 原因:我之所以想做這個項目,是因為在之前查找關於C#/WPF相關資料時,我發現講解圖像濾鏡的資源非常稀缺。此外,我註意到許多現有的開源庫主要基於CPU進行圖像渲染。這種方式在處理大量圖像時,會導致CPU的渲染負擔過重。因此,我將在下文中介紹如何通過GPU渲染來有效實現圖像的各種濾鏡效果。 生成的效果 ...
  • 引言 上一章我們介紹了在xUnit單元測試中用xUnit.DependencyInject來使用依賴註入,上一章我們的Sample.Repository倉儲層有一個批量註入的介面沒有做單元測試,今天用這個示例來演示一下如何用Bogus創建模擬數據 ,和 EFCore 的種子數據生成 Bogus 的優 ...
  • 一、前言 在自己的項目中,涉及到實時心率曲線的繪製,項目上的曲線繪製,一般很難找到能直接用的第三方庫,而且有些還是定製化的功能,所以還是自己繪製比較方便。很多人一聽到自己畫就害怕,感覺很難,今天就分享一個完整的實時心率數據繪製心率曲線圖的例子;之前的博客也分享給DrawingVisual繪製曲線的方 ...
  • 如果你在自定義的 Main 方法中直接使用 App 類並啟動應用程式,但發現 App.xaml 中定義的資源沒有被正確載入,那麼問題可能在於如何正確配置 App.xaml 與你的 App 類的交互。 確保 App.xaml 文件中的 x:Class 屬性正確指向你的 App 類。這樣,當你創建 Ap ...
  • 一:背景 1. 講故事 上個月有個朋友在微信上找到我,說他們的軟體在客戶那邊隔幾天就要崩潰一次,一直都沒有找到原因,讓我幫忙看下怎麼回事,確實工控類的軟體環境複雜難搞,朋友手上有一個崩潰的dump,剛好丟給我來分析一下。 二:WinDbg分析 1. 程式為什麼會崩潰 windbg 有一個厲害之處在於 ...
  • 前言 .NET生態中有許多依賴註入容器。在大多數情況下,微軟提供的內置容器在易用性和性能方面都非常優秀。外加ASP.NET Core預設使用內置容器,使用很方便。 但是筆者在使用中一直有一個頭疼的問題:服務工廠無法提供請求的服務類型相關的信息。這在一般情況下並沒有影響,但是內置容器支持註冊開放泛型服 ...
  • 一、前言 在項目開發過程中,DataGrid是經常使用到的一個數據展示控制項,而通常表格的最後一列是作為操作列存在,比如會有編輯、刪除等功能按鈕。但WPF的原始DataGrid中,預設只支持固定左側列,這跟大家習慣性操作列放最後不符,今天就來介紹一種簡單的方式實現固定右側列。(這裡的實現方式參考的大佬 ...