从壹开始微服务 [ DDD ] 之八 ║剪不断理还乱的 值对象和Dto
缘起
哈喽大家周四好,时间是过的真快,这几天一直忙着在公司的项目,然后带带新人,眼看这周要过去了,还是要抽出时间学习学习,这些天看到群里的小伙伴也都在忙着新学习,还是很开心的,至少当时的初衷已经达到了,一起学习一起进步嘛,哪怕是对现在或者是对以后的工作有一丢丢的帮助,也是不枉此时的努力,哈哈夜里写文章总是容易多想,好啦,废话不多说,上次咱们说到了《从壹开始微服务 [ DDD ] 之七 ║项目第一次实现 & CQRS初探》,今天本来应该接着写 领域命令 了,在设计的领域命令的时候,发现了值对象的存在,对 领域模型 和 视图模型 有着剪不断理还乱的困扰,所以我就暂时单写一篇了,既是对上一篇的补充,又是对领域命令的铺垫,好啦,马上开始今天的说明吧~~
还是老规矩,每篇文章先给大家一个小问题,先思考下,然后有助于理解本文:
问题:我们在领域模型 Student 中,有一个户籍的值对象(为啥叫户籍,下边会说到),然后我们也有一个学生的视图模型 StudentViewModel ,那么问题来了,我们在 StudentViewModel 中,如何去定义这个户籍的视图模型呢,然后又是如何传给领域模型 Student 呢?
1、不写这户籍一块,直接在业务逻辑里,手动赋值给 Student 领域模型
public class StudentViewModel
{ [Required(ErrorMessage = "The Name is Required")]
[MinLength()]
[MaxLength()]
[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()]
[MaxLength()]
[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()]
[MaxLength()]
[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()]
[MaxLength()]
[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
从壹开始微服务 [ DDD ] 之八 ║剪不断理还乱的 值对象和Dto的更多相关文章
- 从壹开始微服务 [ DDD ] 之终篇 ║当事件溯源 遇上 粉丝活动
回首 哈喽~大家好,时间过的真快,关于DDD领域驱动设计的讲解基本就差不多了,本来想着周四再开一篇,感觉没有太多的内容了,剩下的一个就是验证的问题,就和之前的JWT很类似,就不打开一个章节了,而且这个 ...
- 从壹开始微服务 [ DDD ] 之十二 ║ 核心篇【下】:事件驱动EDA 详解
缘起 哈喽大家好,又是周二了,时间很快,我的第二个系列DDD领域驱动设计讲解已经接近尾声了,除了今天的时间驱动EDA(也有可能是两篇),然后就是下一篇的事件回溯,就剩下最后的权限验证了,然后就完结了, ...
- 从壹开始微服务 [ DDD ] 之十一 ║ 基于源码分析,命令分发的过程(二)
缘起 哈喽小伙伴周三好,老张又来啦,DDD领域驱动设计的第二个D也快说完了,下一个系列我也在考虑之中,是 Id4 还是 Dockers 还没有想好,甚至昨天我还想,下一步是不是可以写一个简单的Angu ...
- 从壹开始微服务 [ DDD ] 之三 ║ 简单说说:领域、子域、限界上下文
前言 哈喽大家好,DDD领域驱动设计系列又开始了,前天周二的那篇入门文章中,也收到了一定的效果(写小说的除外),同时我也是倍感鸭梨,怎么说呢,DDD领域驱动设计已经有十年历史了,甚至更久,但是包括我在 ...
- 从壹开始微服务 [ DDD ] 之六 ║聚合 与 聚合根 (下)
前言 哈喽大家周二好,上次咱们说到了实体与值对象的简单知识,相信大家也是稍微有些了解,其实实体咱们平时用的很多了,基本可以和数据库表进行联系,只不过值对象可能不是很熟悉,值对象简单来说就是在DDD领域 ...
- 从壹开始微服务 [ DDD ] 之七 ║项目第一次实现 & CQRS初探
前言 哈喽大家周五好,我们又见面了,感谢大家在这个周五读我的文章,经过了三周的时间,当然每周两篇的速度的情况下,咱们简单说了下DDD领域驱动设计的第一部分,主要包括了,<项目入门DDD架构浅析& ...
- 从壹开始微服务 [ DDD ] 之一 ║ D3模式设计初探 与 我的计划书
缘起 哈喽大家周四好!又是开心的一天,时间过的真快,我们的 <从壹开始 .net core 2.1 + vue 2.5>前后端分离系列共 34 篇已经完结了,当然以后肯定还会有更新和修改, ...
- 从壹开始微服务 [ DDD ] 之九 ║从军事故事中,明白领域命令验证(上)
烽烟 哈喽大家周二好呀,咱们又见面了,上周末掐指一算,距离 圣诞节 只有 5 周的时间了(如果你还不知道为啥我要提圣诞节这个时间点,可以看看我的第二系列开篇<之一 ║ D3模式设计初探 与 我的 ...
- 从壹开始微服务 [ DDD ] 之二 ║ DDD入门 & 项目结构粗搭建
前言 哈喽大家好,今天是周二,我们的DDD系列文章今天正式开始讲解,我这两天一直在学习,也一直在思考如何才能把这一个系列给合理的传递给大家,并且达到学习的目的,还没有特别好的路线,只是一个大概的模糊的 ...
随机推荐
- 嘿嘿嘿,开始自学mysql
开始学习mysql了,作为非计算机专业学生,必须需要一个地方来给自己的知识进行一些记录和总结. 一SQL语句 数据库是不认识java语言的,但是我们同样要与数据库交互,这时需要使用到数据库认识的语言S ...
- python:解析js中常见的 不带引号的key的 json
首先要明晰一点,json标准中,key是必须要带引号的,所以标准json模块解析不带引号的key的 json就会抛错 不过有一些lib可以帮我们解析 如:demjson(链接) >>> ...
- 在Windows Server 2008 R2下搭建jsp环境(二)-JDK的下载安装
因为服务器上的Tomcat的运行环境需要JDK的支持,所以,掌握JDK的安装与下载和配置是一个重要步骤. 1.首先下载最新的JDK版本.网络上提供了最新版本的JDK下载,如图所示.首先选择&quo ...
- dirlock.go
// +build !windows package dirlock import ( "fmt" "os" "syscall ...
- CentOS7系统操作httpd服务 - 开机启动/重启/查看状态
第一.启动.终止.重启 systemctl start httpd.service #启动 systemctl stop httpd.service #停止 systemctl restart htt ...
- 使用Freemarker 实现JSP页面的静态化
使用Freemarker 静态化网页 一.原理 Freemarker 生成静态页面,首先需要使用自己定义的模板页面,这个模板页面可以是最最普通的html,也可以是嵌套freemarker中的 取值表达 ...
- selenium+python+eclipse 实现 “问卷星”网站,登录与检查登录示例!
1.使用selenium+python+eclipse实现的登录"问卷星",问卷星访问地址:https://www.sojump.com/ 2.实现步骤:1)进入链接---首页-- ...
- 咸鱼Chen
关于我 网名:咸鱼Chen 英文:nick chen 签名:I'm nothing but I must be everything. 标签:Python爱好(ma)者(nong),干过后端开发.算法 ...
- 神奇的Scala Macro之旅(三)- 实际应用
在上一篇中,我们示范了使用macro来重写 Log 的 debug/info 方法,并大致的介绍了 macro 的基本语法.基本使用方法.以及macro背后的一些概念, 如AST等.那么,本篇中,我们 ...
- go语言视频教程和电子书下载
golang视频教程: https://noxue.com/p/399809259943301 go语言 pdf电子书: Design Pattern In Go[go语言设计模式].pdf Go P ...