从壹开始微服务 [ 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; }
    }

或许你还有其他啥办法,要是有感觉更好的,或者更正确的,千万要评论留言哟,只不过这三种办法是我亲身实验的,这里大家先思考一下,希望看完本文你会有一些自己的想法。

零、今天实现蓝色的部分

image

一、创建 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.Sucesso = "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>

image

这些都是 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.Sucesso = "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 都是私有的,不能被赋值,不信请看

image

这个时候怎么办,聪明的你肯定能想到一个方法,既然值对象不行,它内部不可变,不能赋值,那我就自己在视图模型中,再写一个 AddressViweModel 不就行啦,然后可以进行set操作,想到这里还是很激动,赶紧试试,这就看看能不能获取到值。

image

很不错,已经把内容获取到了,然后通过视图对象传到Add() 方法,很成功的达到了目的。

image

看来这个方法也是可以的,只不过有一个小问题就是,这里需要多了一个类来实现,如果我不想用类接受,而且是直接用属性呢?那就是第三种办法了,请继续往下看。

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 没有成功。

然后我们提交的时候,发现后端虽然能接受到数据,

image

可是在转换到 Student 的时候失败了:

image

这里显示的是,我们无法对其进行转换,因为在视图模型中,没有匹配到 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页面,户籍信息也出来了

image

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 中的户籍信息,一一匹配到视图模型中的属性。

然后我们测试数据,不仅仅可以把数据获取到,还可以成功的转换过去:

image

最后首页查看验证信息,以及添加上了,完成。

image

四、结语

今天呢,是补充了上一把的坑,一共提供了三个办法,当然其实第一种也不算是方法,主要是后两者,不知道大家是否能看的懂,然后更倾向于哪一种:

2、不用配置 Automapper 映射信息,只需要新建一个一样的户籍值对象的视图模型 —— 户籍视图模型即可,因为结构相同,所以不需要手动配置映射,就能达到目的。

3、只需要一个视图模型即可控制,在某些情况下,我们不方便使用嵌套的复杂视图模型,只需要配置下映射文件即可达到目的。

今天,也为下一篇做准备,怎么说呢,大家发现,现在我们能正确的添加进去了,但是如果我们要进行验证该怎么办?比如说,我们要判断学校不能小于14岁,手机号格式,邮箱格式等等,

当然,你可以说,我会用前端js校验,也可以后端获取到,if 判断,都是可以的,

不过我个人感觉,后端校验还是很需要的,我采用 FluentValidation 进行后端校验,并且融入到 **领域命令 **中,那如何实现呢,下次再见咯~~~

五、GitHub & Gitee

https://github.com/anjoy8/ChristDDD

https://gitee.com/laozhangIsPhi/ChristDDD

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,214评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,307评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,543评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,221评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,224评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,007评论 1 284
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,313评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,956评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,441评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,925评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,018评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,685评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,234评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,240评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,464评论 1 261
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,467评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,762评论 2 345

推荐阅读更多精彩内容

  • 前言 哈喽,老张是周四放松又开始了,这些天的工作真的是繁重,三个项目同时启动,没办法,只能在深夜写文章了,现在时间...
    SAYLINING阅读 4,490评论 1 14
  • 1. Java基础部分 基础部分的顺序:基本语法,类相关的语法,内部类的语法,继承相关的语法,异常的语法,线程的语...
    子非鱼_t_阅读 31,581评论 18 399
  • 宽容、等候、音乐、夏日,字数不超过250字 夏日街头,柏油马路烤得人神情恍惚。张三站在路口,数着红灯秒数,59秒,...
    MioH阅读 91评论 0 0
  • 四月油花不再黄,纷纷卸妆换叶长。 偶有蜜蜂从旁过,误入花心饿得慌。
    简村小吹阅读 213评论 5 6
  • 清晨,蔚蓝色的小溪旁,有一群可爱的小朋友在河边玩耍,有一群小鱼儿在快活的游来游去,有一群美丽的天鹅在展示自己美丽洁...
    幼稚园的鬼阅读 264评论 0 0