從壹開始微服務 [ DDD ] 之二 ║ DDD入門 & 項目結構粗搭建


前言

 

哈嘍大家好,今天是周二,我們的DDD系列文章今天正式開始講解,我這兩天一直在學習,也一直在思考如何才能把這一個系列給合理的傳遞給大家,並且達到學習的目的,還沒有特別好的路線,只是一個大概的模糊的安排,畢竟我沒有做過講師,但是我感覺還是需要對自己負責,至少要對得起這個熬夜寫的博客吧 😀,我簡單設計了下整體流程,可能以后還會變動,不過大致方向是不會變的:

我打算通過一個最簡單一個例子來講,而且這個例子也是《實現領域驅動設計》這一本書的第一個小例子,至於為什么要引用這個小栗子,我想的是,希望把整本書的大致精髓柔和到我這個 Project 中,最好給大家一個感覺,以后大家再看的的時候,不會感覺在天上飄飄然不知其所往,相反,是會感覺似曾相識,會感覺看過,不會那么生硬無趣,這個就是我希望達到的兩個目的的其中之一:在了解並認識DDD領域驅動設計的情況下,也簡單的了解了這邊書的相關內容。

今天要說的是我們平時百分之百會遇到的,就是新建一個實體類,並對其進行CURD操作,看到這里,你也許會胸有成竹的說,已經玩兒的很溜了,當然這是肯定的,今天不是說哪些新知識,而且說一下其實DDD驅動設計,已經在我們平時的開發中體現出來,當然又沒有完全貫徹下來(至少我是這么一個),也說一下在我們的開發當中為什么需要使用DDD領域驅動設計。

這里說明下今天搭建的是一個小雛形我會以后慢慢細化,像上一個系列那樣,逐漸增加功能,差不多最后的框架是這樣的:

 

這里要說明下:
1、平時我們的小項目中,用戶故事和用例流 小於20 的,就不需要使用DDD了,至少是一個中型電商系統以上這種才需要,如果你就一個控制台,或者日志管理系統,那就不需要了。

2、學習新東西不要拘泥於技術,應該重點理解思想,思想不通,就不想學,就會打心里排斥。

更新:

下邊的評論很精彩,希望大家看看,更希望大家來討論討論,好多人說 DDD 領域驅動設計就像多層架構,感覺又像是一個 MVC 架構,我這里要說的是:

多層和MVC 都是一個成型的,很好的框架,注意這里是框架,像一個梯子,簡單+方便+高效+多年驗證可行,但是這僅僅是一個框架,我們拿到一個項目后,就迅速搭建架構了。

但是DDD領域驅動設計,是一個設計思想,更像是一個全自動高效電梯,里邊有算法,有豐富的子領域,我們把專注點放到領域如何划分,如何領域建模,把一些具體的操作封裝在了一個虛實體中,然后同時在寫實體的時候繼承這個虛實體,提供了值對象,給對象增加了狀態等等。當然以后還會有驅動設計,領域+驅動,才是DDD,今天就是一個簡單的領域建模,大家多思考思考。

DDD最大的好處是:接觸到需求第一步就是考慮領域模型,而不是將其切割成數據和行為,然后數據用數據庫實現,行為使用服務實現,最后造成需求的首肢分離。DDD讓你首先考慮的是業務語言,而不是數據。重點不同導致編程世界觀不同。DDD是解決復雜中大型軟件的一套行之有效方式,在國外已經成為主流。DDD認為很多原因造成軟件的復雜性,我們不可能避免這些復雜性,能做的是對復雜的問題進行控制。而一個好的領域模型是控制復雜問題的關鍵。領域模型的價值在於提供一種通用的語言,使得領域專家和軟件技術人員聯系在一起,溝通無歧義。

 

 

零、今天要實現橙色的部分

 

 

 

一、為什么要使用DDD

這個已經是老生常談了,相信大家也稍微或多或少的了解一些,在領域驅動設計中,我們的中心不在代碼是開發技術上,而且在業務上,在這個設計中,會涉及到開發人員和領域專家(可能是項目管理者,或者部門經理,甚至是公司總經理,這些都可以算上領域專家),雖然在使用DDD的時候,需要我們在前期投入較多的時間和精力去考慮如何建模,已經開發過程中會遇到的各種問題,到那時這樣的投入是完全值得的,相信這個系列的結束,大家有一個中肯的評價。

過去系統分析和系統設計都是分離的,正如我們國家“系統分析師” 和“系統設計師” 兩種職稱考試一樣,這樣割裂的結果導致,需求分析的結果無法直接進行設計編程,而能夠進行編程運行的代碼卻扭曲需求,導致客戶運行軟件后才發現很多功能不是自己想要的,而且軟件不能快速跟隨需求變化。

從上圖可以看出來,在項目初期,領域驅動設計是需要最高的努力值,付出的,但是隨着項目復雜度增加,它的辛苦值並沒有受到很大的波動,反之剩下兩個,都是陡值。

 

1、過度耦合

業務初期,我們的功能大都非常簡單,普通的CRUD就能滿足,此時系統是清晰的。隨着迭代的不斷演化,業務邏輯變得越來越復雜,我們的系統也越來越冗雜。模塊彼此關聯,誰都很難說清模塊的具體功能意圖是啥。修改一個功能時,往往光回溯該功能需要的修改點就需要很長時間,更別提修改帶來的不可預知的影響面。

 

 

訂單服務接口中提供了查詢、創建訂單相關的接口,也提供了訂單評價、支付、保險的接口。同時我們的表也是一個訂單大表,包含了非常多字段。在我們維護代碼時,牽一發而動全身,很可能只是想改下評價相關的功能,卻影響到了創單核心路徑。雖然我們可以通過測試保證功能完備性,但當我們在訂單領域有大量需求同時並行開發時,改動重疊、惡性循環、疲於奔命修改各種問題。

上述問題,歸根到底在於系統架構不清晰,划分出來的模塊內聚度低、高耦合。

 

2、增加我們項目的業務價值

在開發中,什么是最寶貴的,當然是開發人員的時間效率最重要,然后還有就是溝通上,如果我們自己自定義的開發出一套代碼,沒有一系列的業務模塊,這樣的話,就是一個純技術的項目,雖然本來領域專家也看不懂,但是如果我們僅僅專注於技術方面,而沒有把業務人員的思想所映射到開發者中,那整個項目也只有可能僅僅是你成為這個領域專家,因為只有開發者當事人才能看懂。

這樣隨着時間的發展,隨着開發者當事人的離職或者轉向其他項目,本應該駐留在軟件中的領域知識也就丟失了。在以后的任何修改代碼中,新的開發人員雖然可以重新和領域專家進行思想的溝通,但是之前開發的代碼已經無法撼動,這也就是為什么現在我們找工作的時候,不喜歡修改別人的代碼,而且也會經常聽到這個情況:盡量不要修改原來的代碼,自己新建一個,嗯,這個更是要命了的。

試想一下,如果我們把注意力重點放到了業務上,這樣就很清晰的進行迭代,而且也可以進一步的與領域專家進行對接。那我們的項目中都需要用到 DDD 領域驅動設計么?答案當時是否定的,如果滿足以下幾點,我建議還是需要使用 DDD 吧:

1、如果你的系統中有30~40個用戶故事或者用例流的時候,軟件的復雜性就出來了。

這里說明下用戶故事和用例流:就比如一個商城,我們有普通用戶,會員,公司管理員,企業合作伙伴,站長等等多個角色,當然還有其他的角色,每一個角色又會有一系列的操作,比如用戶會瀏覽商品,下單,修改個人信息等等一系列的操作,這個就是用例流,這些角色所進行的全部操作,就是用戶故事。

2、如果你的系統現在不是很復雜,但是以后會復雜。就比如我們的商城后台:本來是僅僅的對數據庫表的CURD,但是在真正的用戶(管理員)使用的時候,會發現在商品商家的時候,並不是很方便,需要用到價格規格表,或者發現商家信息已經實現商品的多對多分配(現在可能是一對多),那想想后期的修改是很龐大的。

3、如果你的系統所在的領域不是很清晰,就連領域專家也不是很清晰,需要在未來幾年的慢慢討論,變化中前進的時候。

 

3、貧血症和失憶症

相信大家都聽過這兩個名詞,雖然聽着不是很舒服,但是卻天天在我們的手中被設計出來,如果你說不會,那好,請看下邊兩個情況是否在你的系統中出現(這里說明下,這些栗子都是我現在手中的項目):

1、你的領域對象(就是指實體類對象)是不是主要包含些共有的get 和 set,並且幾乎沒有業務邏輯?比如這樣:

    public class BlogArticle
    {
        public int bID { get; set; }
        public string bsubmitter { get; set; }
        public string btitle { get; set; }
        public string bcategory { get; set; }
        public string bcontent { get; set; }
        public int btraffic { get; set; }
        public int bcommentNum { get; set; }
        public DateTime bUpdateTime { get; set; }
        public System.DateTime bCreateTime { get; set; }
        public string bRemark { get; set; }
    }

2、你的軟件組件(就是指我們的方法定義)中是否經常會用到領域對象來實現一些業務邏輯,而且還是通過 get 和 set 的方式,直接將其拋給服務層或者應用層,甚至是直接拋到頁面上。

        //添加一個車票
        public ActionResult AddTicket(FormCollection form)
        {
            var TitleHead = form["TitleHead"].ToString();
            var txtSubmitter = form["txtSubmitter"].ToString();
            var TicketChannelOperateJson = form["TicketChannelOperateJson"].ToString();

            try
            {
                if (!string.IsNullOrEmpty(TitleHead) && !string.IsNullOrEmpty(TicketChannelOperateJson) && !string.IsNullOrEmpty(txtSubmitter))
                {

                    Tickets tickets = new Tickets
                    {
                        Submiter = txtSubmitter,
                        SubmitDate = DateTime.Now,
                        SubmitData = TicketChannelOperateJson,
                        ActionType = Common.Enums.ActionType.Add,
                        ActionStatus = Common.Enums.ActionStatus.New,
                        Approver = "",
                        LastUpdateDate = DateTime.Now,
                        IsDelete = false,
                        IsCleanUp = false,
                        IsCheckIn = false,
                        TicketType = TicketType.Channel,
                    };

                    TicketsBLL ticketsBLL = new TicketsBLL();

                    bool flag = ticketsBLL.Add(tickets) > 0;
                }
            }
            catch (Exception e)
            {
                Logger.Log(e.InnerException + e.Message);
            }

        }

如果兩個情況你都說的是 NO ,那么恭喜你,你的代碼設計很健康,沒有檢查出來有貧血問題,如果你的回答中有一個 YES,那是不存在的,如果是兩個YES,我們就請繼續往下看吧。

4、內存模型和數據庫模型保持一致

三層架構的最大問題在於:實際應用中人們喜歡把內存模型和數據庫模型保持一致。三層架構的大部分問題都是從這里衍生出來的。

數據庫模型的粒度如果很小,那么大量的表連接很快就會讓數據庫跑不動了。

如果數據庫模型的粒度如果很大(這是大部分項目的選擇),代碼的質量(重用性、穩定性、擴展性)就很差。由於沒有從業務的角度去仔細定義每一個對象,每個人會根據自己的需要建立各種QueryModel或ViewModel,慢慢地類會多到想哭。或如果不建立各種Model,強行重用DataModel的話,那么接口提供的內容往往絕大部分都不是你想要的。

內存模型與數據庫模型保持一致並非天生的,這是有很多原因造成的:

它建模的簡單性讓初學者無法拒絕,由於經驗主義,以至於多年以后已經沒有勇氣去擺脫了;

沒有專門論述三層與建模的書籍;

ORM工具誤導,與數據表結構一致內存結構方便建立映射關系;

示范代碼的誤導,錯誤把示范代碼當成產品代碼;

等等......

種種誤導導致很多人工作很多年后依然未能找到正確的路,忽略了一個重要的核心(業務建模)環節(業務模型要與代碼的數據結構保持一致),以至於讓人產生無論你去到那個公司都是一樣的錯覺(在很多人的經歷中,這的確是事實,包括我自己),但事實不應該是這樣的。用戶界面,領域模型,數據庫它們應該具有同等的重要位置,領域模型在很多公司都是被忽略的角色。

由於目前的服務層職責是非常輕的,甚至有很多空殼的調用,所以平衡一下職責,把調用數據訪問層的職責從業務邏輯層提升到服務層,需要的數據通過參數傳遞給業務邏輯層。這樣,對於那些簡單到無業務邏輯的CRUD就不需要去訪問業務層了,直接調用數據訪問層。我們就可以把業務邏輯與業務實體移到一塊,然后把屬於業務實體的邏輯遷移到實體類中。

上面很簡單就把問題搞定了,但是實際遷移過程中,風險是非常大的,如果沒有充分掌握重構知識,建議不要在正式產品代碼上嘗試,這個場景你一定遇到過,數據庫模型設計好了,但是業務中需要用到新的屬性,但是這個卻不是數據模型的一部分,我們這個時候就只能修改數據庫模型——實體類了。

 

二、貧血對象對我們做了什么

如果你上邊的都已經看明白了,那我們就開始建我們的項目了,當然這里要說明下,以后會對項目進行修繕,前幾章的代碼可能很 low ,甚至是不好的,只是為了能說明問題。

1、新建一個 .net core web 項目

在指定的文件夾下,新建一個 Christ3D 解決方案,然后再新建一個 web 項目,具體過程相信大家都會了,如果不會,請會看第一個系列《系列教程一目錄:.netcore+vue 前后端分離

 

2、在Models 文件夾中,新增我們的領域對象 Customer.cs 和持久化虛擬類 CustomerDao.cs

    /// <summary>
    /// Customer 領域對象
    /// </summary>
    public class Customer
    {
        public string Id { get; set; }
        public string Name { get; set; }
        public string Email { get; set; }
        public DateTime BirthDate { get; set; }
    }


    /// <summary>
    /// 領域對象持久化層
    /// </summary>
    public class CustomerDao
    {
        public static Customer GetCustomer(string id)
        {
            return new Customer() { Id = "1", Name = "Christ", Email = "Christ@123.com" };
        }


        public static string SaveCustomer(Customer customer)
        {
            return "保存成功";
        }
    }

 

3、在默認的 HomeController.cs 控制器中,添加保存顧客方法 saveCustomer()

/// <summary>
/// 保存顧客方法:add & update
/// </summary>
/// <param name="id"></param>
/// <param name="name"></param>
/// <param name="email"></param>
/// <param name="birthDate"></param>
public void saveCustomer(string id, string name, string email, string birthDate)
{
    Customer customer = CustomerDao.GetCustomer(id);
    if (customer == null)
    {
        customer = new Customer();
        customer.Id = id;
    }

    if (name != null)
    {
        customer.Name = name;
    }
    if (email != null)
    {
        customer.Email = email;
    }

    //...還有其他屬性

    CustomerDao.SaveCustomer(customer);
}

 

到這里,這就是一個領域 —— 對顧客領域對象進行保存,這個功能還是很強大的,不管一個 Customer 是新增還是更新,甚至是不管名字換了,還是郵件重新申請了,都可以在這里體現,相信這樣的方法我們都寫過,至少我是這樣。

這個方法真的很強大么,這里我們看一下,我們並不知道 saveCustomer() 的具體的應用場景(是后台管理,還是前台用戶自己操作,還是更新手機號等等),經過幾周或者幾年后,我們完全不知道當時創建這個方法的本來目的和意圖,當然你會說可以通過注釋的方法,我表示是反對的,我們對程序的設計,不能依賴於注釋,這是很可怕的。

還有,這些方法嵌套在一起,我們不能對其進行單元測試,都不用說細節,可能數據庫的約束就能對這個方法造成崩潰:比如時間字段,比如字符串長度過長,比如不能為空等,而且因為有很多地方調用這個看似功能強大的基方法,我們需要一個一個的往上游去研究,看看到底是哪個地方,哪個方法,哪個業務邏輯調用了,可能需要幾個小時的時間,甚至更久。

總結來說,上邊的 saveCustomer() 方法存在三個弊端:

1、saveCustomer() 方法的業務意圖不明確,當時我們為了貪圖功能的強大,把注意力都放到了技術上,從而忽略了業務核心。

2、saveCustomer() 方法的實現本來就增加了潛在的復雜性,雖然看着強大了,可是復雜度卻直線向上。

3、Customer 顧客領域對象根本不是對象,充其量就是一個數據持有者,僅僅是把我們的數據從持久化的數據庫中拿出來到內容的一個工具。

 那既然如此,我們需要怎么辦呢,沒錯,就是我們平時使用到的分層,一個專注領域業務的分層 —— DDD領域驅動設計,就出現了。

 

三、一切皆從領域開始 —— Domain

還記得下邊這個圖么,這個是我上一篇文章中提到的《領域驅動設計架構圖》,我個人表示,這個圖已經很貼切和較為詳細的表示了DDD領域驅動設計是整體框架圖,大家從整體的箭頭走向就能看清楚(誰指向誰,表示前者依賴后者后者實現前者),其中的內容我們都會說到,今天先簡單的把這個整體框架搭起來,至少先從什么的DDD框架講起來——當然也就是從被依賴最多的領域層開始。

從上邊的文章中,我們知道了,在軟件開發中,我們已經把重點放到領域業務上,在上邊的 saveCustomer() 的方法中,所有的邏輯和用例都一股腦的放在一起,完全背離了這個思想,所以,那我們如果想要通過領域設計的思路來創建,會是怎么樣的呢,請往下看。

1、定義領域對象 Customer.cs(值對象/聚合/根,以后會說)

在解決方案中,新建 .net core 類庫 Christ3D.Domain ,作為我們的領域層(這是一個臃腫的領域層,以后我們會把領域核心給抽象出來,現在簡化是為了說明),然后在該層下,新建 Models 文件夾,存放我們以后的全部領域對象,我們的專業領域設計,都是基於領域對象為基礎。

老張:這里並沒有增加業務邏輯,以后會說明

 /// <summary>
    /// 定義領域對象 Customer
    /// </summary>
    public class Customer
    {
        protected Customer() { }
        public Customer(Guid id, string name, string email, DateTime birthDate)
        {
            Id = id;
            Name = name;
            Email = email;
            BirthDate = birthDate;
        }

        public Guid Id { get; private set; }
        public string Name { get; private set; }
        public string Email { get; private set; }
        public DateTime BirthDate { get; private set; }
    }

 

2、定義泛型接口 IRepository.cs

這里說下為什么開發中都需要接口層:

在層級結構中,上層模塊調用下層模塊提供的服務,這里就會存在一種依賴關系,Rebort C. Martin提出了依賴倒置原則大致是如下:

上層模塊不應該依賴於下層模塊,兩者都應該依賴於抽象;

抽象不應該依賴於實現,實現應該依賴於抽象;

這是一個面向接口編程的思想。

在我們的領域層下,新建 Interfaces 文件夾,然后添加泛型接口

在我們專注的領域業務中,我們只需要定義該領域Customer 的相關用例即可(就比如如何CURD,如何發郵件等等,這些都是用戶角色Customer的用例流),而不用去關心到底是如何通過哪種技術來實現的,那種ORM去持久化的,這就是領域設計的核心,當然現在有很多小伙伴還是喜歡直接把接口和實現放在一起,也無可厚非,但是不符合DDD領域設計的思想。

可能這個時候你會說,領域層,定義接口和實現方法放在一起也可以嘛,現在我們是看不出來效果的,以后我們會在這里說到領域驅動,領域通知,事件驅動等等知識點的時候,你就會發現,在Domain層來對接口進行實現是那么格格不入,沒關系慢慢來~~~

    /// <summary>
    /// 定義泛型倉儲接口,並繼承IDisposable,顯式釋放資源
    /// </summary>
    /// <typeparam name="TEntity"></typeparam>
    public interface IRepository<TEntity> : IDisposable where TEntity : class
    {
        /// <summary>
        /// 添加
        /// </summary>
        /// <param name="obj"></param>
        void Add(TEntity obj);
        /// <summary>
        /// 根據id獲取對象
        /// </summary>
        /// <param name="id"></param>
        /// <returns></returns>
        TEntity GetById(Guid id);
        /// <summary>
        /// 獲取列表
        /// </summary>
        /// <returns></returns>
        IQueryable<TEntity> GetAll();
        /// <summary>
        /// 根據對象進行更新
        /// </summary>
        /// <param name="obj"></param>
        void Update(TEntity obj);
        /// <summary>
        /// 根據id刪除
        /// </summary>
        /// <param name="id"></param>
        void Remove(Guid id);
        /// <summary>
        /// 保存
        /// </summary>
        /// <returns></returns>
        int SaveChanges();
    }

 

3、定義 Customer 領域接口 ICustomerRepository.cs

 我們以后的每一個子領域中特有的接口,還是需要定義的,並且繼承自我們的泛型倉儲接口

    /// <summary>
    /// ICustomerRepository 接口
    /// 注意,這里我們用到的業務對象,是領域對象
    /// </summary>
    public interface ICustomerRepository : IRepository<Customer>
    {
        //一些Customer獨有的接口
 Customer GetByEmail(string email);
}

好了,這個時候我們的最最最簡單的領域層就搭建好了,里邊有我們的子領域 Customer的相關接口實現,整體結構是這樣的:

 

 

四、基礎層對領域進行實現 —— Infrastructure 

 上邊咱們定義了 Domian 領域層,這是一個接口層,那我們必須來實現它們,大家可以再往上看那個DDD領域驅動設計架構圖,你應該能找到,是誰對領域層進行了實現,答案當然是基礎設施層。

我們在解決方案下,新建一個 Christ3D.Infrastruct.Data 類庫,你一定會問,為什么不直接是 Christ3D.Infrastruct ,反而在后邊還需要多一個 .Data 后綴呢,這里要說的就是,在這個基礎設施層中,會有很多很多的內容,比如驗證層,IoC層,事務,工作單元等等,以后都會說到,至少從名字上你也能明白——基礎設施,這些基礎東西是不能放在領域層的,這是肯定的,因為我們關心的是業務領域,這個不是我們的業務,也不會是下文的應用層,至於為什么,你可以先想一想,數據驗證和AOP這些為何不放在應用層

1、新建泛型倉儲 Repository 

Repository繼承了IRepository接口,這里我們先不寫具體的實現,我們先定義好方法體,以后再慢慢填上,大家還記得如何快速的實現接口吧,Ctrl+.  可能有的小伙伴沒有這個功能,那就只能手動了。

 

 

    /// <summary>
    /// 泛型倉儲,實現泛型倉儲接口
    /// </summary>
    /// <typeparam name="TEntity"></typeparam>
    public class Repository<TEntity> : IRepository<TEntity> where TEntity : class
    {
        public void Add(TEntity obj)
        {
            throw new NotImplementedException();
        }

        public void Dispose()
        {
            throw new NotImplementedException();
        }

        public IQueryable<TEntity> GetAll()
        {
            throw new NotImplementedException();
        }

        public TEntity GetById(Guid id)
        {
            throw new NotImplementedException();
        }

        public void Remove(Guid id)
        {
            throw new NotImplementedException();
        }

        public int SaveChanges()
        {
            throw new NotImplementedException();
        }

        public void Update(TEntity obj)
        {
            throw new NotImplementedException();
        }
    }

 

2、實現子領域Customer 倉儲

  /// <summary>
    /// Customer倉儲,操作對象還是領域對象
    /// </summary>
    public class CustomerRepository : Repository<Customer>, ICustomerRepository
    {
        //對特例接口進行實現
        public Customer GetByEmail(string email)
        {
            throw new System.NotImplementedException();
        }
    }

最終結構

 

相信大家看到這里,基本還是很輕松的,我們在領域層,把業務定義清楚,把領域對象設計好,然后在基礎層對其進行實現,是一個很好的過程,這么看還是可以的。

但是一定會有小伙伴會看不慣這么寫,他會說,領域設計定義接口我懂,那就定義,定義實現也可以,比如用 EFCore 或者其他 ORM 框架,那這樣直接在展示層調用不就好了,為啥還需要單拿出來一個應用 Application 層呢(等同於我們之前的 Service 層),到時候肯定又多出來一套接口和實現的過程,麻煩不麻煩?

我在上一個系列教程中,本來也是這么嘗試使用DDD領域驅動設計,可以中間沒有看《實現領域驅動設計》這本書,導致出現了漏洞,也被各種小伙伴吐槽,這個系列就再證明一下吧,具體有什么好處,或者說為什么要在基礎設施層之上,再增加一個應用層(也就是我們的 Service 層),這里先不說,下邊請繼續看。

  

 

五、定義系統的業務功能 —— Application

如果Repository 應用在應用層,會出現什么情況:這樣就致使應用層和基礎層(我把數據持久化放在基礎層了)通信,忽略了最重要的領域層,領域層在其中起到的作用最多也就是傳遞一個非常貧血的領域模型,然后通過 Repository 進行“CRUD”,這樣的結果是,應用層不變成所謂的 BLL(常說的業務邏輯層)才怪,另外,因為業務邏輯都放在應用層了,領域模型也變得更加貧血。 

Application為應用層(也就是我們常說的 Service 層),定義軟件要完成的任務,並且指揮表達領域概念的對象來解決問題。這一層所負責的工作對業務來說意義重大,也是與其它系統的應用層進行交互的必要渠道。應用層要盡量簡單,不包含業務規則或者知識,而只為下一層中的領域對象協調任務,分配工作,使它們互相協作。它沒有反映業務情況的狀態,但是卻可以具有另外一種狀態,為用戶或程序顯示某個任務的進度。

1、視圖模型——Rich 領域模型(DTO以后說到)

在文章的最后,咱們再回顧下文章開頭說的貧血對象模型,相信你應該還有印象,這個就是對剛剛上邊這個問題最好的回答,如果我們直接把展示層對接到了基層設施層,那我們勢必需要用到領域模型來操作,甚至是對接到視圖里,不僅如此,我們還需要驗證操作,傳值操作等等,那我們又把領域對象模型過多的寫到了業務邏輯里去,嗯,這個就不是DDD領域驅動設計了,所以我們需要一個應用層,對外進行數據接口的提供,這里要強調一點,千萬不要把應用層最后寫滿了業務邏輯,業務應該在領域層!!!

在項目根路徑下,新建 Christ3D.Application 類庫,作為我們的應用層,然后新建 ViewModels 文件夾,用來存放我們的基於UI 的視圖模型,它是如何來的,這個下邊說到。

 

 /// <summary>
    /// 子領域Customer的視圖模型
    /// </summary>
    public class CustomerViewModel
    {
        [Key]
        public Guid Id { get; set; }

        [Required(ErrorMessage = "The Name is Required")]
        [MinLength(2)]
        [MaxLength(100)]
        [DisplayName("Name")]
        public string Name { get; set; }

        [Required(ErrorMessage = "The E-mail is Required")]
        [EmailAddress]
        [DisplayName("E-mail")]
        public string Email { get; set; }

        [Required(ErrorMessage = "The BirthDate is Required")]
        [DisplayFormat(ApplyFormatInEditMode = true, DataFormatString = "{0:yyyy-MM-dd}")]
        [DataType(DataType.Date, ErrorMessage = "Data em formato inválido")]
        [DisplayName("Birth Date")]
        public DateTime BirthDate { get; set; }
    }

這里僅僅是增加了特性,更多的業務邏輯還是在 領域層 來實現的。

 

2、定義應用服務接口 ICustomerAppService,依賴抽象思想

 在我們的應用層下,新建 Interfaces 文件夾,用來存放我們的對外服務接口,然后添加 Customer服務接口類,這里要說明下,在應用層對外接口中,我們就不需要定義泛型基類了,因為已經沒有必要,甚至是無法抽象的,

    /// <summary>
    /// 定義 ICustomerAppService 服務接口
    /// 並繼承IDisposable,顯式釋放資源
    /// 注意這里我們使用的對象,是視圖對象模型
    /// </summary>
    public interface ICustomerAppService : IDisposable
    {
        void Register(CustomerViewModel customerViewModel);
        IEnumerable<CustomerViewModel> GetAll();
        CustomerViewModel GetById(Guid id);
        void Update(CustomerViewModel customerViewModel);
        void Remove(Guid id);
    }

 

3、實現應用服務接口 CustomerAppService.cs ,對接基層設施層

 在我們的應用層下,新建 Services 文件夾,用來存放我們對服務接口的實現類

    /// <summary>
    /// CustomerAppService 服務接口實現類,繼承 服務接口
    /// 通過 DTO 實現視圖模型和領域模型的關系處理
    /// 作為調度者,協調領域層和基礎層,
    /// 這里只是做一個面向用戶用例的服務接口,不包含業務規則或者知識
    /// </summary>
    public class CustomerAppService : ICustomerAppService
    {
        private readonly ICustomerRepository _customerRepository;

        public CustomerAppService(ICustomerRepository customerRepository)
        {
            _customerRepository = customerRepository;
        }

        public IEnumerable<CustomerViewModel> GetAll()
        {
            return null;
            //return _customerRepository.GetAll().ProjectTo<CustomerViewModel>();
        }

        public CustomerViewModel GetById(Guid id)
        {
            return null;
            //return _mapper.Map<CustomerViewModel>(_customerRepository.GetById(id));
        }

        public void Register(CustomerViewModel customerViewModel)
        {
            //var registerCommand = _mapper.Map<RegisterNewCustomerCommand>(customerViewModel);
        }

        public void Update(CustomerViewModel customerViewModel)
        {
            //var updateCommand = _mapper.Map<UpdateCustomerCommand>(customerViewModel);
        }

        public void Remove(Guid id)
        {
            //var removeCommand = new RemoveCustomerCommand(id);
        }

        public void Dispose()
        {
            GC.SuppressFinalize(this);
        }
    }

目前這里還沒有具體使用基礎層的倉儲,為什么呢,因為應用層是面向視圖對象模型,不涉及到業務,而基礎設施層和領域層是基於 領域對象模型,面向業務的,所以我們需要用到 DTO ,這一塊以后我們會說到。

 

六、結語

 好啦,今天的講解基本就到這里了,到目前為止,僅僅是實現了DDD領域驅動設計的第一個 D 領域模型,還沒有說到驅動的概念,通過經典的DDD四層,大家應該也了解了各層的作用,這里有簡單的三個問題,不知道你是否已經真的看懂了,如果都能回答上來,恭喜!如果不是很確定,那抱歉,還需要再看看,或者查資料看書,或者來群里咨詢我吧。

1、什么是貧血對象模型?

2、我們的業務接口和業務實現,分別在哪一層?( Answer:領域層和基礎設施層 )

3、為什么還需要定義一個領域層,而不是直接在應用層(Service層)對接基礎層(Repository層)?

好啦,周四我們會繼續推進,DDD 領域對象設計是如何實現 領域、子域、界限上下文的

 

七、Github & Gitee

https://github.com/anjoy8/ChristDDD

https://gitee.com/laozhangIsPhi/ChristDDD 


免責聲明!

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



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