1. 程式人生 > 其它 >戲說領域驅動設計(廿一)——領域服務

戲說領域驅動設計(廿一)——領域服務

實體物件和值物件都寫完了,本想開始寫資源倉庫順便把工作單元再搞搞。不過有一點麻煩的是我不太想把工作單元作為單獨的一章來寫,一是這東西網上相關的內容太多;二是有的時候使用Spirng的事務就解決了,沒覺得有多大作用。不過先不糾結這些,還是按本章的主題寫領域服務吧,這好講,誰不喜歡簡單的東西。

一、為啥引入領域服務

要說原因吧,其實也挺簡單的。我們在開發過程中有些方法你就不知道放哪個物件身上合適。比如說經典的轉賬業務,你是放客戶物件上不合適,客戶物件中沒有餘額的概念;放賬戶上面吧,感覺也差了點意思,這裡不僅涉及到賬戶自身的業務,還涉及到了另外的賬戶。能把自己整明白就不錯了,您還管得著別人?還有的時候,一些方法所做的事情不是業務邏輯而是轉換,這種糙活誰誰都不願意幹。哪個物件都有自己的矜持與驕傲,你讓他幹這些活,那是看不起人家。總而言之吧,涉及多跨領域物件的操作、物件轉換、沒人願意幹的活就交給領域服務這個老好人,這傢伙是系統中的中間人、和事老。可您不能小瞧人家,領域服務也有自己的體面的,幹得活再雜也是BO層中的大佬。不都得圍著人家轉。而且呢,你別看他不起眼兒,關鍵時間方能顯出英雄本色,要不然你的程式碼就是一坨那個……

二、領域服務的責任

BO層中有些東西雖然看著不重要,可不能簡單的輕看人家,那幹得活絕對不含糊。您看上面這個圖,領域服務幹得事兒不少呢。不過你用的時候可也得注意兩點:一是和應用服務的分開,後面那東西雖然重要可他不處理業務;我們領域服務雖然沒有實體風光,那也是處理業務的,鬧呢?;二是在領域服務中,你可不能訪問或使用DAO、Ioc、Spring甚至是Log,我和這些人都是老死不相往來的。

實體、值物件、資源倉庫介面及領域服務,作為BO中的四公子之一,領域服務是最為謙虛的君子。不像實體那麼張揚,不像資源倉庫那麼粗劣。然而成也蕭何敗也蕭何。你用得好多了,領域模型變成了貧血模型;用少了,程式碼看著亂七八糟,隨隨便便。不偏不倚才能讓設計的東西更加有組織、有原則,說以我們得重點說說需要在什麼情況下如何用領域服務。

二、如何使用

首先,領域服務一般是不包含屬性的。如果你在設計過程中發現需要加屬性,那此時你就得開始考慮一下是不是應該把方法和屬性放到某個實體或值物件中,只有後者兩才是承擔屬性的物件了。那位說了,ThreadLocal行不?肯定不行啊,你的服務中都不能包含屬性,用ThreadLocal有個毛線用?為了少寫點程式碼,您用的時候就把領域服務建模為單例(不需要使用volatile關鍵字,這東西用多了容易引入匯流排風暴)就OK了,不用非得先new再呼叫例項方法,多麻煩。

第二,如果某個業務需要跨領域模型或者說是跨實體實現,最好也用領域服務。這條約束作為開發規範在團隊中進行要求是有意義的。您還記得我在前面說過開發過程中會涉及到三類服務:應用服務、資料服務和領域服務。這仨東西有一個共同的責任:“流程組織”,前兩者用於組織用例的控制流程和資料的操作流程,而領域服務則用於組織業務規則。他通過協調多個領域物件的互動來完成某個業務,屬於指揮官的角色。比如下列的程式碼:我在建立新的優惠策略的時候需要判斷系統中不能存在重複的優惠,相當於將目標優惠與現存優惠做衝突檢測,很明顯這項業務是跨領域模型的,所以我建立了一個服務來完成任務。

public class DiscountConflictsService {

    final public static DiscountConflictsService INSTANCE = new DiscountConflictsService();

    private DiscountConflictsService() {

    }

    /**
     * 優惠條件衝突檢測
     * @param target 目標優惠
     * @param sources 優惠列表
     */
    public List<Discount> testConflictions(Discount target, List<Discount> sources) {
        if (target == null || sources == null) {
            return new ArrayList<Discount>();
        }
        List<Discount> result = this.compareWithExists(target, sources);
        //比較優惠自身是否也有策略衝突
        if (target.hasConflict()) {
            result.add(target);
        }

        Collections.reverse(result);
        return result;
    }
}

在繼續之前我們來說一下在微服務架構下,這領域服務要怎麼玩兒。其實也不用說跨服務的互動,假如某個業務需要跨兩包的物件進行參與要怎麼寫程式碼?本質上跨包與跨服務是一樣的處置方式。看過前面的文章您應該記得我說過的領域模型是不應該跨包的,兩個包之前的互動只能通過檢視模型。讓我們來舉個實際的案例,要不然這麼說太虛。還是以下單為例,在下單前我需要判斷使用者是否未被凍結且已經實名認證,好多人下意識的做法是這樣的。

@Service
public class OrderService {
    @Resource
    private AccountService accountService;
    
    public void placeOrder(OrderDetail orderDetail, String accountId) {
        AccountVO account = this.accountService.queryById(accountId);
        if (account.getStatus() == AccountStatus.FREEZEN) {
            ……
        }
        if (!account.isAuthenticated()) {
            ……
        }
        Order order = OrderFactoryService.create(orderDetail, account);
        ……
    }    
}

這段程式碼其實不太符合OOP的要求,面向過程的味道太重了。在我們使用OOP的時候,如果您想程式碼更加的純粹,在應用服務中所操作的應該全都是領域模型。而在上面的示例中,“AccountVO”是檢視模型,“Order”和“OrderFactoryService”是領域模型,這三個混著玩差了點意思。當然你也可以質問我:不是你說的嗎?兩個包之前不能相互訪問領域模型,我已經傳過來了檢視模型,你又說我的程式設計四不像,找事兒唄?

這事兒要怎麼說呢,我覺得下面這段還是很重要的,您最好拿個小本本兒記下來。我之前寫過的本系系的第十九篇“外驗”中提到過說把驗證抽象成業務規則從應用服務的程式碼中單獨剝離出去,下面這段思想與之並不衝突,請務必注意。上段我說過,在應用服務中所操作應該都是領域模型,那麼你就麼從領域模型的角度來考慮。在賬戶上下文中,我們往往會將賬戶建立成實體,所關注的大概是這幾個方面:使用者基本資訊、聯絡方式、狀態、級別、是否實名、積分等。大部分電商系統中的賬戶都差不多會關心上述資訊。