從壹開始微服務 [ DDD ] 之八 ║剪不斷理還亂的 值對象和Dto


緣起

哈嘍大家周四好,時間是過的真快,這幾天一直忙着在公司的項目,然后帶帶新人,眼看這周要過去了,還是要抽出時間學習學習,這些天看到群里的小伙伴也都在忙着新學習,還是很開心的,至少當時的初衷已經達到了,一起學習一起進步嘛,哪怕是對現在或者是對以后的工作有一丟丟的幫助,也是不枉此時的努力,哈哈夜里寫文章總是容易多想,好啦,廢話不多說,上次咱們說到了《從壹開始微服務 [ DDD ] 之七 ║項目第一次實現 & CQRS初探》,今天本來應該接着寫 領域命令 了,在設計的領域命令的時候,發現了值對象的存在,對 領域模型 和 視圖模型 有着剪不斷理還亂的困擾,所以我就暫時單寫一篇了,既是對上一篇的補充,又是對領域命令的鋪墊,好啦,馬上開始今天的說明吧~~

還是老規矩,每篇文章先給大家一個小問題,先思考下,然后有助於理解本文:

問題:我們在領域模型 Student 中,有一個戶籍的值對象(為啥叫戶籍,下邊會說到),然后我們也有一個學生的視圖模型 StudentViewModel ,那么問題來了,我們在 StudentViewModel 中,如何去定義這個戶籍的視圖模型呢,然后又是如何傳給領域模型 Student 呢?

1、不寫這戶籍一塊,直接在業務邏輯里,手動賦值給 Student 領域模型

    public class StudentViewModel
    {

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

        //... 等等其他,只是學生的個人信息,不涉及戶籍地址
    }

2、和領域模型一樣,也寫一個對象,甚至直接就用領域模型中的 Address 值對象

    public class StudentViewModel
    {
        [Key]
        public Guid Id { get; set; }

        [Required(ErrorMessage = "The Name is Required")]
        [MinLength(2)]
        [MaxLength(100)]
        [DisplayName("Name")]
        public string Name { get; set; }
        //... 等等其他的信息
  

        //這個就是在領域模型Student中使用的,戶籍值對象
        public Address Address { get; set; } 
 
    }

3、把 Address 屬性拆開,一個一個的放在視圖模型 StudentViewModel 中

    public class StudentViewModel
    {
        [Key]
        public Guid Id { get; set; }

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

        //...  等等其他學生信息,比如手機號,郵箱等

/// <summary> /// 城市 /// </summary> public string City { get; set; }//注意這里可以進行set 賦值操作,和值對象不是一回事 /// <summary> /// 區縣 /// </summary> public string County { get; set; } /// <summary> /// 街道 /// </summary> public string Street { get; set; } }

或許你還有其他啥辦法,要是有感覺更好的,或者更正確的,千萬要評論留言喲,只不過這三種辦法是我親身實驗的,這里大家先思考一下,希望看完本文你會有一些自己的想法。

 

零、今天實現藍色的部分

 

 一、創建 Student 的添加模塊

 話說上次咱們是把領域模型(包括實體和值對象)通過EFCore保存到了數據庫,然后也查詢出來了相應的學習信息,(這里注意下,學習的戶籍信息還沒有取出來),這里說一下為什么是戶籍地址信息,

上篇文章中,有小伙伴還是對這個不是很理解,一直想着要一定和數據庫對應上,比如說,為啥叫地址,那如果學生有多個地址咋辦;再比如,這樣修改學生信息,值對象就會發生變化呀,這樣就不能滿足值對象不可變的特性;等等諸如此類的疑問,這里說一下:

1、值對象其實就是一個值,它和Name、Phone、Email等等一模一樣,只不過它是一個對象,復雜了一些,有了自己的內部結構,所以說,值對象是沒有狀態的,沒有唯一標識(多個學生叫張三 == 兩個學生一個地址),是內部不可變性,就比如我們修改一個學校省份,需要將整個值對象都修改,這和修改Name是一樣的。

2、值對象是一個領域中孕育出來的概念,千萬不要事事都要和數據庫,數據模型,扯上關系,如果想要一個會員多個地址,那這個時候地址就是一個實體,甚至是一個聚合了,比如物流地址,這也就是我為什么要把這個Address稱之為 戶籍 的原因了,從領域出發,而不要再和數據模型數據庫表相提並論了。

那咱們就先添加學生的 Create 模塊

1、在 StudentController 中添加 Create Action

        // GET: Student/Create
        // 頁面
        public ActionResult Create()
        {
            return View();
        }

        // POST: Student/Create
        // 方法
        [HttpPost]
        [ValidateAntiForgeryToken]
        public ActionResult Create(StudentViewModel studentViewModel)
        {
            try
            {
                // 視圖模型驗證
                if (!ModelState.IsValid)
                    return View(studentViewModel);
                // 執行添加方法
                _studentAppService.Register(studentViewModel);

                ViewBag.success = "Student Registered!";

                return View(studentViewModel);
            }
            catch(Exception e)
            {
                return View(e.Message);
            }
        }

這個時候大家肯定都已經很熟悉了,而且 Service 層注入什么的,相信大家已經得心應手了,這里都不細說了。

 

2、創建 Create View頁面

@model Christ3D.Application.ViewModels.StudentViewModel
@{
    ViewData["Title"] = "Register new Student";
}
<h2>@ViewData["Title"]</h2>
<form asp-action="Create">
    <div class="form-horizontal">
        <hr />
        @* Replacing classic Validation Summary to Custom ViewComponent as TagHelper *@
        <vc:summary />
        <div class="form-group">
            <label asp-for="Name" class="col-md-2 control-label"></label>
            <div class="col-md-10">
                <input asp-for="Name" class="form-control" />
                <span asp-validation-for="Name" class="text-danger"></span>
            </div>
        </div>
        <div class="form-group">
            <label asp-for="Email" class="col-md-2 control-label"></label>
            <div class="col-md-10">
                <input asp-for="Email" class="form-control" />
                <span asp-validation-for="Email" class="text-danger"></span>
            </div>
        </div>
        <div class="form-group">
            <label asp-for="Phone" class="col-md-2 control-label"></label>
            <div class="col-md-10">
                <input asp-for="Phone" class="form-control" />
                <span asp-validation-for="Phone" class="text-danger"></span>
            </div>
        </div>
        <div class="form-group">
            <label asp-for="BirthDate" class="col-md-2 control-label"></label>
            <div class="col-md-10">
                <input asp-for="BirthDate" class="form-control" />
                <span asp-validation-for="BirthDate" class="text-danger"></span>
            </div>
        </div>
        <div class="form-group">
            <div class="col-md-offset-2 col-md-10">
                <input type="submit" value="Create" class="btn btn-success" />
                <a asp-action="Index" class="btn btn-info">Back to List</a>
            </div>
        </div>
    </div>
</form>

 

這些都是 AspNetCore.Mvc.ViewFeature 的模型命令還有驗證等,相比以前的模型,已經有很大的改善了,這個可以自己試試,很簡單,直接往下走,重頭戲來了。

這個時候,如果我們添加信息保存的話,一定會發現一個問題,就是戶籍信息到底如何傳入呢,上邊說的三種辦法到底該選擇哪一種呢,下邊咱們一一來實驗下。

 

二、如何把值對象添加到視圖模型

 這個時候肯定會有小伙伴說,為什么一定要把值對象放到視圖模型中,就比如文章的第一個方法,我就不放進去,我從頁面內獲取到Country、Province、City等等后,然后再傳到領域模型不就行了,真的么?

1、手動賦值的方法

 假設我們已經從前台頁面內獲取到了戶籍信息,然后我們就會這么做(紅色部分

  public ActionResult Create(StudentViewModel studentViewModel,string country,string provice,string city,string street)
        {
                // 視圖模型驗證
                if (!ModelState.IsValid)
                    return View(studentViewModel);
                //這個時候還需要對戶籍信息進行驗證判斷
                //比如字符串不能數字,字符啥的

                // 執行添加方法,把戶籍信息傳遞過去                
_studentAppService.Register(studentViewModel,country, provice, city, street);
ViewBag.success = "Student Registered!"; return View(studentViewModel); }

Stop!相信我,你肯定不會這么做的,當然,偶爾偶爾我們會這么接受一個參數,也偶爾會這么寫,可是這么寫肯定是不行的,且不說不是DDD領域驅動設計思想,就連OOP思想也沒有發揮起來,所以方法一直接pass。

這個時候我們開始思考,至少需要把戶籍信息放到視圖模型 StudentViewMode 中吧,嗯看着文章開頭的第二個方法就特別好!對象是吧,這個可是真是的OOP思想,全部用對象接收參數,然后把數據傳如到倉儲的Add()方法中,這樣就直接保存了嘛,多好呀!想想的心動,那就開始吧,一個小坑正在慢慢變大。

 

2、用對象的方法將值對象添加到視圖模型中

聽着很拗口,說白了,就是文章開頭的第二種方法,領域模型和視圖模型,共用一個 值對象。然后我們修改下 view 頁面,用來傳遞參數。

        <div class="form-group">
            <label asp-for="BirthDate" class="col-md-2 control-label"></label>
            <div class="col-md-10">
                <input asp-for="BirthDate" class="form-control" />
                <span asp-validation-for="BirthDate" class="text-danger"></span>
            </div>
        </div>

        <div class="form-group">
            <label asp-for="Address.County" class="col-md-2 control-label"></label>
            <div class="col-md-10">
                <input asp-for="Address.County" class="form-control" />
                <span asp-validation-for="Address.County" class="text-danger"></span>
            </div>
        </div>
        <div class="form-group">
            <label asp-for="Address.Province" class="col-md-2 control-label"></label>
            <div class="col-md-10">
                <input asp-for="Address.Province" class="form-control" />
                <span asp-validation-for="Address.Province" class="text-danger"></span>
            </div>
        </div>
        <div class="form-group">
            <label asp-for="Address.City" class="col-md-2 control-label"></label>
            <div class="col-md-10">
                <input asp-for="Address.City" class="form-control" />
                <span asp-validation-for="Address.City" class="text-danger"></span>
            </div>
        </div>

這個時候,我們一定很歡喜,然后點擊提交,發現,無論怎么提交都不會在

  public ActionResult Create(StudentViewModel studentViewModel)

中獲取到我們需要的戶籍信息,天哪!這是啥情況,當然是獲取不到的,因為 Address 是一個值對象,具有不可變性,它的 set 都是私有的,不能被賦值,不信請看

這個時候怎么辦,聰明的你肯定能想到一個方法,既然值對象不行,它內部不可變,不能賦值,那我就自己在視圖模型中,再寫一個 AddressViewModel 不就行啦,然后可以進行set操作,想到這里還是很激動,趕緊試試,這就看看能不能獲取到值。

 

 

 很不錯,已經把內容獲取到了,然后通過視圖對象傳到Add() 方法,很成功的達到了目的。

 

看來這個方法也是可以的,只不過有一個小問題就是,這里需要多了一個類來實現,如果我不想用類接受,而且是直接用屬性呢?那就是第三種辦法了,請繼續往下看。

 

3、用屬性字段來講戶籍信息放到視圖模型中

就是文章開頭的第三種辦法,這樣的:

 public class StudentViewModel
    {
        
        [Required(ErrorMessage = "The Name is Required")]
        [MinLength(2)]
        [MaxLength(100)]
        [DisplayName("Name")]
        public string Name { get; set; }
        
        //... 其他

        /// <summary>
        /// 省份
        /// </summary>
        [Required(ErrorMessage = "The Province is Required")]
        [DisplayName("Province")]
        public string Province { get; set; }

        /// <summary>
        /// 城市
        /// </summary>
        public string City { get; set; }

        /// <summary>
        /// 區縣
        /// </summary>
        public string County { get; set; }

        /// <summary>
        /// 街道
        /// </summary>
        public string Street { get; set; }
    }

然后再修改下頁面里的調用情況,直接用調用屬性

 <div class="form-group">
     <label asp-for="Province" class="col-md-2 control-label"></label>
     <div class="col-md-10">
         <input asp-for="Province" class="form-control" />
         <span asp-validation-for="Province" class="text-danger"></span>
     </div>
 </div>

這個時候,我們滿懷開心的運行項目的時候,發現,index頁面的戶籍信息沒有了,也就是說 Student -> StudentViewModel 的時候,通過 Automapper 沒有成功。

 

然后我們提交的時候,發現后端雖然能接受到數據,

 

可是在轉換到 Student 的時候失敗了:

 

 

 這里顯示的是,我們無法對其進行轉換,因為在視圖模型中,沒有匹配到 Student 的 Address 值對象信息,不要慌,下邊我們會說這個問題。

 

三、Automapper實現復雜對象的轉換

 為了解決上一個問題,我研究了下 Automapper 官網,發現,這種復雜拷貝,需要進行手動配置,其實也是很簡單,只需要創建匹配屬性即可
 注意,在第二種方法中是不需要配置的,因為第二種方法,兩個模型結構幾乎一模一樣,這第三種方法,結構已經變了,一個是對象,一個僅僅是一個屬性值。

1、復雜領域模型轉換到視圖模型

 /// <summary>
 /// 配置構造函數,用來創建關系映射
 /// </summary>
 public DomainToViewModelMappingProfile()
 {
     CreateMap<Student, StudentViewModel>()
         .ForMember(d => d.County, o => o.MapFrom(s => s.Address.County))
         .ForMember(d => d.Province, o => o.MapFrom(s => s.Address.Province))
         .ForMember(d => d.City, o => o.MapFrom(s => s.Address.City))
         .ForMember(d => d.Street, o => o.MapFrom(s => s.Address.Street))
         ;

  
 }

 這個時候,我們看Index頁面,戶籍信息也出來了

 

 

2、視圖模型轉換到復雜領域模型

 public ViewModelToDomainMappingProfile()
 {
     //手動進行配置
     CreateMap<StudentViewModel, Student>() .ForPath(d => d.Address.Province, o => o.MapFrom(s => s.Province)) .ForPath(d => d.Address.City, o => o.MapFrom(s => s.City)) .ForPath(d => d.Address.County, o => o.MapFrom(s => s.County)) .ForPath(d => d.Address.Street, o => o.MapFrom(s => s.Street)) ; }

這里將 Student 中的戶籍信息,一一匹配到視圖模型中的屬性。

 然后我們測試數據,不僅僅可以把數據獲取到,還可以成功的轉換過去:

 

最后首頁查看驗證信息,以及添加上了,完成。

 

四、結語

 今天呢,是補充了上一把的坑,一共提供了三個辦法,當然其實第一種也不算是方法,主要是后兩者,不知道大家是否能看的懂,然后更傾向於哪一種:

2、不用配置 Automapper 映射信息,只需要新建一個一樣的戶籍值對象的視圖模型 —— 戶籍視圖模型即可,因為結構相同,所以不需要手動配置映射,就能達到目的。

3、只需要一個視圖模型即可控制,在某些情況下,我們不方便使用嵌套的復雜視圖模型,只需要配置下映射文件即可達到目的。

 今天,也為下一篇做准備,怎么說呢,大家發現,現在我們能正確的添加進去了,但是如果我們要進行驗證該怎么辦?比如說,我們要判斷學校不能小於14歲,手機號格式,郵箱格式等等,

當然,你可以說,我會用前端js校驗,也可以后端獲取到,if 判斷,都是可以的,

不過我個人感覺,后端校驗還是很需要的,我采用 FluentValidation 進行后端校驗,並且融入到 領域命令 中,那如何實現呢,下次再見咯~~~

 

五、GitHub & Gitee

https://github.com/anjoy8/ChristDDD

https://gitee.com/laozhangIsPhi/ChristDDD 

 


免責聲明!

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



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