當前位置:
首頁 > 知識 > 領域服務與應用服務的區別

領域服務與應用服務的區別

在這篇文章中,我們將看一下領域域服務與應用服務有什麼不同。

人們常說,領域服務是承載那些不自然地適合實體和值對象的領域知識。但是,還有另一個原因可能需要引入域服務。這個原因與領域模型隔離有關。

那麼,領域服務與應用服務有何不同?這兩個概念都假設無狀態類可以在領域實體和值對象之上工作,但這幾乎與它們的相似性有關。它們之間的主要區別在於領域服務包含領域邏輯而應用服務不包含領域邏輯。

領域邏輯是與業務策略相關的所有內容。因此,領域服務以與實體和值對象相同的方式參與策略實施過程(業務規則)。而應用服務是實現由實體和值對象所做出的編排方式編排。

我們來看這個例子:

public void WithdrawMoney(decimal amount)
{
_atm.DispenseMoney(amount);
decimal amountWithCommission =_atm.CalculateAmountWithCommission(amount);

_paymentGateway.ChargePayment(amountWithCommission);
_repository.Save(_atm);
}

方法withdrawMoney方法是應用服務的一部分,並且包括一個面向客戶的API。它告訴ATM實體首先分配一些金額,然後要求它用傭金進行金額計算,將計算結果通過支付網關收費,最後將實體保存到資料庫。

此應用程序服務看起來不錯,還是應該將一些代碼從其中提煉出來放到領域服務中呢?讓我們來看看。

在前兩行中,該方法使用Atm領域實體做出一些業務決策。在最後兩行中,它將這些決定轉換為可見的副作用:調用支付網關並修改資料庫的狀態。

這裡的前兩行是關於將決策過程委託給領域模型。是否需要將前兩行有關領域知識的代碼提煉到領領域服務中?例如,像這樣:

public void WithdrawMoney(decimal amount)
{
decimal amountWithCommission =_atmService.DispenseAndCalculateCommission(
_atm, amount);

_paymentGateway.ChargePayment(amountWithCommission);
_repository.Save(_atm);
}
public sealed class AtmService // Domain service
{
public decimal DispenseAndCalculateCommission(Atmatm, decimal amount)
{
atm.DispenseMoney(amount);
returnatm.CalculateAmountWithCommission(amount);
}
}

不能這樣做,使用多個與領域模型相關的代碼行這一事實並不構成領域知識。重要的是這些代碼行是否負責做出業務決策。

在上面的示例中,DispenseAndCalculateCommission方法的圈複雜度為1(一種代碼複雜度的衡量標準),沒有類似if語句導致的分支以表示代碼需要做出任何判斷(這就代表業務決策),它只是要求領域實體做兩件事。

而且代表做這兩件事的兩種方法的調用順序也無關緊要。如果我們要重新安排它們的調用順序,將傭金計算查詢放到分配貨幣命令之前(這代表邏輯順序變化,說明兩者不是互為因果的邏輯關係,不是邏輯就不是領域知識核心),那麼在Atm的不變數方面沒有任何改變,它仍將保持有效。這是一個強有力的跡象,也沒有泄漏的實施細節。

現在,讓我們稍微更改代碼示例,驗證一下:

public void WithdrawMoney(decimal amount)
{
if (!_atm.CanDispenseMoney(amount))
return;

_atm.DispenseMoney(amount);
decimal amountWithCommission =_atm.CalculateAmountWithCommission(amount);

_paymentGateway.ChargePayment(amountWithCommission);
_repository.Save(_atm);
}

此示例中的圈複雜度高於1,因為我們現在有一個「if」語句。這不是說明應用服務現在已經包含領域知識嗎?

不,實際的決策過程仍然存在於Atm。由實體單獨決定是否可以分配任何資金。應用程序服務只是協調該決定,並繼續執行或不執行。

只要Atm中的DispenseMoney方法有一個先決條件,說明CanDispenseMoney在分配現金之前必須保持為真,所有不變數都會受到保護。前提條件本身可以像這樣簡單地實現:

public void DispenseMoney(decimal amount)
{
if (!CanDispenseMoney(amount))
throw new InvalidOperationException();

/* … */
}

因此,即使應用服務忽略CanDispenseMoney做出的決定,Atm實體也不會進入不一致狀態。它將拋出一個例外。

何時提取到領域服務?

上面示例中的應用服務不做出任何業務決策,它將這些決策委託給領域模型。請注意,領域模型是隔離的:Atm實體不會將自身保存到資料庫,也不會通過支付網關直接收費。我們在這裡有一個很好的關注點分離:業務邏輯放入領域模型,而與外部世界的交互 - 應該放入應用服務。

您可以注意到大多數遵循此指南的代碼庫中的模式。他們的執行流程如下:

  • 準備業務操作所需的所有信息:從資料庫載入參與實體並從其他外部源檢索所需的所有數據。
  • 執行操作。該操作由領域模型做出的一個或多個業務決策組成。這些決定導致更改模型的狀態,或生成一些結果(上面示例中的amountWithCommission值)。
  • 將操作結果應用於外界。

只有第1步和第3步涉及與外部依賴關係的工作。在第一步中檢索到數據下接著實現第二步:接受的入參數及其生成的輸出僅包含實體,值對象和基本類型。

請注意,在簡單的CRUD應用程序中,沒有第二步,因為沒有做出決定的地方。在這種情況下,所有操作都可以僅由應用服務執行,無需將它們委託給領域模型。實際上,不存在任何富領域模型。在這種情況下,貧血領域模型也可以正常工作。

現在,讓我們將代碼示例修改為更現實的場景。假設由於餘額不足而導致支付費用失敗,如果發生這種情況,我們不應該分配任何現金。代碼:

public void WithdrawMoney(decimal amount)
{
if (!_atm.CanDispenseMoney(amount))
return;

decimal amountWithCommission =_atm.CalculateAmountWithCommission(amount);
Result result =_paymentGateway.ChargePayment(amountWithCommission);

if (result.IsFailure)
return;

_atm.DispenseMoney(amount);
_repository.Save(_atm);
}

在這個版本中,我們引入的第二個「if」語句確實代表了領域邏輯。它決定我們是否會向用戶分發現金。但是,與第一個條件運算符不同,不是Atm實體做出該決定。這是應用服務本身。

即使付款在此之前失敗,現在也可以從Atm獲取現金,領域實體不為我們保證此不變性了,並且在不違反實體隔離的情況下引入這樣的不變性是不可能的,因為為了檢查這個前提條件,它必須調用第三方服務。

那麼,在這種情況下該怎麼辦?這是領域服務可以提供幫助的地方。你可以將任何需要來自外部世界的額外信息的業務決策放入領域服務,並且因為以下原因而無法放入實體和值對象中實現:

public void WithdrawMoney(decimal amount)
{
Atm atm = _repository.Get();
_atmService.WithdrawMoney(atm, amount);
_repository.Save(_atm);
}
public sealed class AtmService // Domain service
{
public void WithdrawMoney(Atm atm, decimal amount)
{
if (!atm.CanDispenseMoney(amount))
return;

decimal amountWithCommission = atm.CalculateAmountWithCommission(amount);
Result result =_paymentGateway.ChargePayment(amountWithCommission);

if (result.IsFailure)
return;

atm.DispenseMoney(amount);
}
}

這裡的領域服務是周邊細節與複雜性/業務邏輯之間的中間地帶。一方面,我們無法完全隔離此服務,因為它必須與支付網關一起工作才能完成其工作。另一方面,我們並沒有將太多的領域邏輯放入其中,只有關於如何兌換現金以獲得信貸的知識。

我們仍然將儘可能多的邏輯放入實體中。例如,分配現金的行為仍然是Atm的責任。我們仍然嘗試儘可能地隔離域服務。例如,我們不會使它與存儲庫一起工作,因為它不需要做出業務決策。引入服務的雜質和領域邏輯在這裡是最低限度的。足以讓它正常工作。

這種提取可能看起來有問題。這不僅僅是責任的轉移而沒有實際的好處嗎?但是,有一些好處。領域服務中的代碼比應用程序服務中的代碼更易於測試。它具有較少的外部依賴性,因此我們需要使用較少的測試雙精度來對其進行單元測試。這項服務當然不像實體那樣可測試,但仍然如此。第二個好處是,這樣我們可以防止領域知識泄漏,並將所有領域邏輯保留在領域模型邊界內,這可能有助於提高可讀性。

我不會說在這個特定的例子中,這兩個好處起了很大作用。在應用服務本身中保留一些不適合實體的邏輯並且不會每次都引入單獨的領域服務,這是非常好的。但是,請確保此邏輯不會重複,並且不太複雜。如果您發現違反DRY原則或應用服務變得過於複雜,您需要明確引入領域服務。

將領域服務注入實體

我有時會聽到的一個問題是:領域服務是否可以注入實體?

我個人區分兩種類型的域服務:純(隔離)和不純(非隔離)。前者純的意思表示實體和價值對象是閉合的,不依賴於外部世界。而AtmService是一個不純的域服務的示例。

向實體注入不純的領域服務會破壞隔離,因此我建議不要這樣使用。另一方面,純領域服務不會造成任何傷害,因此從您的實體和值對象中引用它們是完全正確的。

總結

  • 領域服務帶有領域知識; 應用服務沒有(理想情況下)。
  • 領域服務包含不自然地適合放入實體和值對象的領域邏輯。
  • 當你發現某些邏輯無法放入實體/值對象時,引入領域服務,否則會破壞它們與外界的隔離性。

領域服務與應用服務的區別

打開今日頭條,查看更多精彩圖片
喜歡這篇文章嗎?立刻分享出去讓更多人知道吧!

本站內容充實豐富,博大精深,小編精選每日熱門資訊,隨時更新,點擊「搶先收到最新資訊」瀏覽吧!


請您繼續閱讀更多來自 程序員小新人學習 的精彩文章:

websocket實現用戶雙方通信
solr 數據導入中unable to read:dataimport.properties的解決

TAG:程序員小新人學習 |