DDD領域驅動設計初探(六):領域服務


前言:之前一直在搭建項目架構的代碼,有點偏離我們的主題(DDD)了,這篇我們繼續來聊聊DDD里面另一個比較重要的知識點:領域服務。關於領域服務的使用,書中也介紹得比較晦澀,在此就根據博主自己的理解談談這個知識點的使用。

DDD領域驅動設計初探系列文章:

一、領域服務的引入

在《領域驅動設計:軟件核心復雜性應對之道.Eric.Eva》這本書中,作者這樣定義領域服務:有些重要的領域操作,不適合歸到實體(Entity)和值對象(Value Object)的類別中,這些操作從本質上講是有些活動或動作,而不是事物,當領域中的某個重要過程或轉換操作不屬於實體或值對象的自然職責時,應該在模型中添加一個作為獨立接口的操作,並將其申明為Service

Service是作為接口提供的一種操作,它在模型中是獨立的,與實體和值對象不同的是,它強調的是與其他對象的關系,只定義能夠為客戶做什么。一般情況下,對於那些放在哪個實體和值對象里面都不合適,並且它更加偏向的是實體和實體之間的關系,這種場景下,我們就需要使用領域Service。看到這里,有人就郁悶了,既然領域服務的作用是處理實體與實體之間的關系,那么為什么不把它放在應用層里面去呢,應用層的作用不就是協調任務的嗎?的確,理論上來講,放在應用層也能夠解決,但博主的理解是,DDD的設計原則之一是盡量豐滿領域模型,主張充血的領域模型,也就是說和領域模型相關的領域邏輯最好是方法領域層,很顯然,處理實體和實體之間的關系一般來說領域邏輯,所以最好還是放在領域層。當然,這個也並不絕對,只是博主自己的理解。

我們還是來舉個例子說明,比如我們權限管理里面,如果需要一個功能,將指定的用戶賦予制定的權限,比如接口這樣定義

void AssignPower(TB_USERS oUser, TB_ROLE oRole)

按照我們前面聚合的划分,這個接口是應該放在用戶這個聚合里面還是角色聚合里面呢?很顯然,感覺都不合適,這個接口包含兩個聚合的實體,所以像這種情況,可以考慮用領域服務去解決。如果你非要說,方法不這樣設計,放在應用層里面也是可以的。我只能說,呵呵,別鬧。

 二、領域服務的使用

 還記得我們介紹倉儲的時候我們領域層里面的Services文件夾么?下面,我們就根據給指定的用戶賦予制定的權限的功能一步一步寫一個領域服務功能。

1、聚合划分的修改

這里需要提出一個問題,對於博主在C#進階系列——DDD領域驅動設計初探(一):聚合這篇里面聚合的划分,現在想來存在不合理的地方,上次說了應該划分為4個聚合 ,可是沒有考慮到用戶和角色是多對多的關系,所以需要將用戶角色表TB_USERROLE單獨划分為一個聚合,所以總共是5個聚合,這也就是前面說的對於DDD里面聚合的划分是比較考量程序員經驗的,對於聚合划分有誤造成的誤解表示抱歉。我需要在相應的地方做下修改。

領域實體要繼承聚合根基類:

    public partial class TB_USERROLE:AggregateRoot
    {
    }

新建倉儲接口和倉儲實現

    public interface IUserRoleRepository : IRepository<TB_USERROLE>
    {
    }
    [Export(typeof(IUserRoleRepository))]
    public class UserRoleRepository : EFBaseRepository<TB_USERROLE>, IUserRoleRepository
    {
    }

2、領域服務代碼的搭建

在領域層的Services文件夾建立服務的接口和實現

    public interface IPowerManagerDomainService
    {
        void AssignPower(TB_USERS oUser, TB_ROLE oRole);
    }
復制代碼
  [Export(typeof(IPowerManagerDomainService))]
    public class PowerManagerDomainService:IPowerManagerDomainService
    {
        private IUserRepository _userRepository = null;
        private IRoleRepository _roleRepository = null;
        private IUserRoleRepository _userroleRepository = null;


        [ImportingConstructor]
        public PowerManagerDomainService(IUserRoleRepository oUserRoleRepository)
        {
            _userroleRepository = oUserRoleRepository;
        }

        public void AssignPower(TB_USERS oUser, TB_ROLE oRole)
        {
            if (oUser == null || oRole == null)
            {
                return;
            }
            var oUserRole = _userroleRepository.Find(x => x.USER_ID == oUser.USER_ID && x.ROLE_ID == oRole.ROLE_ID).FirstOrDefault();
            if (oUserRole == null)
            {
                oUserRole = new TB_USERROLE();
                oUserRole.ROLE_ID = oRole.ROLE_ID;
                oUserRole.USER_ID = oUser.USER_ID;
                oUserRole.ID = Guid.NewGuid().ToString();
                _userroleRepository.Insert(oUserRole);
            }
        }
    }
復制代碼

在領域服務的實現類PowerManagerDomainService里面,我們使用了MEF的[ImportingConstructor]導入構造函數特性,使用這個特性的前提是構造函數里面的參數類型IUserRoleRepository必須標注了導出Export,由前面我們看到IUserRoleRepository的實現類是標記了導出的,所以通過該特性就可以順利將用戶角色的倉儲實現類的實例導入進來。

3、調用測試

在應用層里面我們來寫測試代碼:

復制代碼
   class Program
    {

        [Import(AllowDefault=false,AllowRecomposition=true,RequiredCreationPolicy=CreationPolicy.Any,Source=ImportSource.Any)]
        public IPowerManagerDomainService powerDomainService { get; set; }
static void Main(string[] args) { var oProgram = new Program(); Regisgter.regisgter().ComposeParts(oProgram); var oUser = new TB_USERS() { USER_ID = "04acd48a819447d388b20dffb15f672e" }; var oRole = new TB_ROLE() { ROLE_ID = "cccc" }; oProgram.powerDomainService.AssignPower(oUser, oRole); Console.ReadKey(); } }
復制代碼

這里需要說明一點:雖然領域服務的實現類PowerManagerDomainService定義了一個有參的構造函數,如果不適用IOC注入的方式,我們通過new這個對象的時候需要傳入倉儲對象,但由於使用了構造函數的導入,參數通過構造函數的導入而傳進去了,所以在應用層使用的時候也不用關心構造函數參數的問題了。這一點博主也是糾結了半天,后台調試程序才知道。是不是這樣的,我們來看看

我們在有參的構造函數里面打一個斷點來看看

構造函數的參數就是這樣傳進去的。方法的執行都是很簡單的邏輯。

4、總結

(1)MEF對於有參構造函數的導入要使用[ImportingConstructor]特性,參數類型要可導入。

(2)由於領域服務的接口和實現都是放在領域層里面,而且領域服務里面調用了倉儲的實現邏輯,那么這樣看領域層又和倉儲的實現揉到一起了,這樣是否又會造成領域層的不純潔了呢?答案是不會,我們看領域服務實現類PowerManagerDomainService里面的代碼可知,里面的邏輯都是通過倉儲的接口對象IUserRoleRepository _userroleRepository去處理的,也就是說領域服務里面並沒有調用具體的倉儲實現,還是和倉儲的接口在打交道,倉儲的實現是在運行的時候通過MEF動態注入進去的。所以這樣設計依然可以保持領域層的純潔,不會依賴於具體的倉儲實現。

(3)在上面的例子中,既然只用到IUserRoleRepository這一個倉儲接口,那么這個功能是否可以直接放在用戶角色的倉儲實現里面而不用使用領域服務呢?剛開始博主也有這種疑惑,可是后來想想還是不行,因為對於權限模塊,UI前端是不會和像DTO_TB_USERROLE這種對象打交道的,因為它里面只有用戶ID和角色ID,對於UI來說沒有實際的意義。所以UI里面只會傳遞用戶和角色這種對象,而這兩種對象隸屬於不同的聚合,這種情況下最好還是使用領域服務了。當然你也可以將TB_USERROLE、TB_USERS、TB_ROLE這3個划分為一個聚合,把TB_USERROLE作為聚合根,另外兩個當做實體,然后利用TB_USERROLE的導航屬性來處理TB_USERS和TB_ROLE,這種做法理論上也行,但是博主覺得像導航屬性這種東西很多項目考慮到效率問題可能會關閉掉,所以使用起來也有一定的風險。還是那句話,視情況而定,如果你的項目利用倉儲基本能搞定需求,或者說你不想又引入一個什么領域服務的概念,你也可以遵照你的設計,這個都沒有問題,畢竟最好的架構是適合項目的架構!

 

源碼下載。有興趣看看!


免責聲明!

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



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