如何運用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
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...