本系列目录:Abp介绍和经验分享-目录

这篇是之前翻备忘录发现漏了的,前阵子刚好同事又提及过这个问题,这里补上。

本文重点在于理解什么是值对象的不可变性。

Abp的ValueObject以及EF的ComplexType

Abp中对应DDD概念的值对象有个基类:ValueObject<T>

这个基类默认重写了EqualsGetHashCode等用于比较两个实例是否相等的方法和重载了==!=操作符。

在构建一些比较复杂的实体时,我们可以把属于同一个概念的多个属性或字段封装成一个值对象。

这个值对象在实体中又对应EntityFramework的复杂类型ComplexType

所以在内存中或者在数据库中,这个对象都可以作为一个整体被赋值、复制或修改

如果不加控制,修改很有可能发生,但是,这类对象,必须设计成不可变的!不能被修改

用两个测试用例举个反例

还是Personball.Demo解决方案。

这已经是我的御用示例项目了,我本地git库保留master分支为当初下载的原始zip文件解压后的源码,新开一篇文章就建个新分支折腾。

我们假设一个场景:

我们有一个创业者A的住址信息,他要开办一家公司A,由于资源不足,他希望把自己的住址登记成公司的办公地址。

我们在Personball.Demo.Core项目根目录加两个值对象Address1Address2

public class Address1 : ValueObject<Address1>
{
public string RegionCode { get; set; } public string Street { get; set; }
} public class Address2 : ValueObject<Address2>
{
protected Address2()
{
//for orm
} //只能通过ctor构造
public Address2(string regionCode, string street)
{
RegionCode = regionCode;
Street = street;
} //setter 被保护起来了
public string RegionCode { get; protected set; }
//setter 被保护起来了
public string Street { get; protected set; }
}

新建Creators目录,加实体Creator:

public class Creator : Entity
{
public Address1 HouseAddress1 { get; set; } public Address2 HouseAddress2 { get; set; }
}

新建Companies目录,加实体Company:

public class Company : Entity
{
public string Name { get; set; } public Address1 OfficeAddress1 { get; set; } public Address2 OfficeAddress2 { get; set; }
}

Personball.Demo.Tests项目中新建目录Companies加测试文件Company_Tests

如果测试资源管理器无法发现单元测试用例,可以删掉临时目录%TEMP%\VisualStudioTestExplorerExtensions,再重启VS即可。

第一个测试用例:

[Fact]
public void HouseAddress1_Should_Not_Be_Modified1()
{
var creatorA = new Creator
{
HouseAddress1 = new Address1
{
RegionCode = "100100",
Street = "xxxx路xxxx号101。"
},
HouseAddress2 = new Address2("100100", "xxxx路xxxx号101。")
}; var companyA = new Company
{
Name = "xxx初创公司",
//公司地址用A的住址,合情合理
OfficeAddress1 = creatorA.HouseAddress1,
OfficeAddress2 = creatorA.HouseAddress2
}; //迭代N次后,可能会有这种需求(办公地址后面追加个公司名称)
companyA.OfficeAddress1.Street += companyA.Name; //断言失败,creatorA.HouseAddress1.Street已被修改!
//creatorA.HouseAddress1.Street.ShouldBe("xxxx路xxxx号101。"); //是同一个实例!
creatorA.HouseAddress1.ShouldBeSameAs(companyA.OfficeAddress1);
}

不要吐槽上面这个生造的需求,“办公地址后面加个公司名称”,只是表达这个意思:

我们很可能在维护了几个月代码后,不经意间,会将一个实体的一个值对象赋值给另一个实体,另一个实体又紧接着修改了自己的这个值对象中的某个属性。

也可能加了这行代码“办公地址后面加个公司名称”的已经是另一个人了。

这个问题如果发生了,很难定位排查。

那么如何防止这种情况发生?

第二个测试用例:

[Fact]
public void HouseAddress2_Should_Not_Be_Modified2()
{
var creatorA = new Creator
{
HouseAddress1 = new Address1
{
RegionCode = "100100",
Street = "xxxx路xxxx号101。"
},
HouseAddress2 = new Address2("100100", "xxxx路xxxx号101。")
}; var companyA = new Company
{
Name = "xxx初创公司",
OfficeAddress1 = creatorA.HouseAddress1,
OfficeAddress2 = creatorA.HouseAddress2 //不经意就会这么干
}; //迭代N次后,不小心可能会这么干
//编译器报错,setter无法访问!
//companyA.OfficeAddress2.Street += companyA.Name; //想改就必须new一个!
companyA.OfficeAddress2 =
new Address2(companyA.OfficeAddress2.RegionCode,
companyA.OfficeAddress2.Street + companyA.Name); //断言通过,creatorA.HouseAddress2.Street不受影响!
creatorA.HouseAddress2.Street.ShouldBe("xxxx路xxxx号101。"); //不同实例!
creatorA.HouseAddress2.ShouldNotBeSameAs(companyA.OfficeAddress2);
}

当我们用了Address2,其属性的setter都被protected限制了从外部直接赋值时,companyA.OfficeAddress2.Street不能被直接修改了!

想修改它时,必须new一个新实例!这时候,对creatorA.HouseAddress2自然是没有影响的。

这并不是特别高深的原理(很基础的OOP知识点),但是能从根本上预防上述问题。

总结

值对象必须被设计成不可变的,当你(或者其他人)想修改它时,必须new一个新实例!

值对象必须被设计成不可变的,当你(或者其他人)想修改它时,必须new一个新实例!

值对象必须被设计成不可变的,当你(或者其他人)想修改它时,必须new一个新实例!

重要的事说三遍,刚才说了预防问题的原理很简单,这个导致问题的原理其实也很简单。

就是OOP编程语言,其主要类型都是引用类型,变量hold的大多时候都是一个地址。

很多时候都是地址传来传去,一个不注意,修改对象的影响范围是在你预料之外的。

因此,OOP语言基本都有限制可访问性的关键字(基于类的铁定有,基于原型的没仔细研究过,不确定)

话说回来,如果有几年工作经验了,却发现好多语言层面的关键字被冷落,是不是该反思下。。。

如果说从贫血模型到充血模型是一次成长,从充血模型到重新审视语言层面提供的特性,又是一次成长。

最后啰嗦一句,其实实体的各种属性更应该被保护起来,限制必须通过方法去修改

否则,你如何保证以后维护代码的三个月后的自己或者其他人会遵守之前的业务规则?

如果是主从关系的多个实体,那就通过聚合根去约束,更复杂的,通过领域服务去约束。

Over.

[2017-09-04]Abp系列——为什么值对象必须设计成不可变的的更多相关文章

  1. 应用程序框架实战十六:DDD分层架构之值对象(介绍篇)

    前面介绍了DDD分层架构的实体,并完成了实体层超类型的开发,同时提供了验证方面的支持.本篇将介绍另一个重要的构造块——值对象,它是聚合中的主要成分. 如果说你已经在使用DDD分层架构,但你却从来没有使 ...

  2. DDD分层架构之值对象(介绍篇)

    DDD分层架构之值对象(介绍篇) 前面介绍了DDD分层架构的实体,并完成了实体层超类型的开发,同时提供了验证方面的支持.本篇将介绍另一个重要的构造块——值对象,它是聚合中的主要成分. 如果说你已经在使 ...

  3. ABP框架 - 值对象

    文档目录 本节内容: 简介 值对象基类 最佳实践 简介 “一个表示领域的一个描述性方面的没有概念上的身份对象,称为值对象.“(Eric Evans). 与一个有身份(Id)实体相反,一个值对象没有身份 ...

  4. [Abp vNext 源码分析] - 5. DDD 的领域层支持(仓储、实体、值对象)

    一.简要介绍 ABP vNext 框架本身就是围绕着 DDD 理念进行设计的,所以在 DDD 里面我们能够见到的实体.仓储.值对象.领域服务,ABP vNext 框架都为我们进行了实现,这些基础设施都 ...

  5. DDD之3实体和值对象

    图中是一个别墅的模型,代表实体,可以真实的看得到.那么在DDD设计方法论中,实体和值对象是什么呢? 背景 实体和值对象是领域模型中的领域对象,是组成领域模型的基础单元,一起实现实体最基本的核心领域逻辑 ...

  6. ABP文档翻译--值对象

    本人是ABP初学者,在看英文文档和@tkb至简 的ABP框架理论研究总结(典藏版)时,发现大神@tkb至简中少了对Value Objects的翻译,看文档是新的,大神没时间把,小弟给补充上. 介绍 值 ...

  7. 《Entity Framework 6 Recipes》中文翻译系列 (44) ------ 第八章 POCO之POCO中使用值对象和对象变更通知

    翻译的初衷以及为什么选择<Entity Framework 6 Recipes>来学习,请看本系列开篇 8-4  POCO中使用值对象(Complex Type--也叫复合类型)属性 问题 ...

  8. DDD理论学习系列(7)-- 值对象

    DDD理论学习系列--案例及目录 1.引言 提到值对象,我们可能立马就想到值类型和引用类型.而在C#中,值类型的代表是strut和enum,引用类型的代表是class.interface.delega ...

  9. ABP官方文档翻译 3.2 值对象

    值对象 介绍 值对象基类 最佳实践 介绍 "展现领域描述性层面且没有概念性身份的对象称之为值对象."(Eric Evans). 和实体相反,实体有身份标示(Id),值对象没有身份标 ...

随机推荐

  1. 使用Go和Let's Encrypt证书部署HTTPS

    为什么要使用HTTPS?使用HTTPS的途径有哪些?如何用Go来部署HTTPS?拿出你的小本本,你要的干货都在这儿! HTTPS的好处我们已在之前的文章中提高好多.它加密浏览器和服务器之间的流量,保障 ...

  2. 关于JavaScript组件化的探索

    Loaders 先放出项目地址:https://github.com/j20041426/Loaders 这是一个可以动态选择加载动画的样式和颜色的插件.这个项目仅仅是作为对js组件化的一个探索,不太 ...

  3. C#多线程之旅(7)——终止线程

    先交代下背景,写<C#多线程之旅>这个系列文章主要是因为以下几个原因:1.多线程在C/S和B/S架构中用得是非常多的;2.而且多线程的使用是非常复杂的,如果没有用好,容易造成很多问题.   ...

  4. 到底什么样的企业才适合实施SAP系统?

    SAP系统作为全宇宙第一的ERP,号称世界500强里面有80%的企业部署了SAP系统,总部位于德国沃尔多夫市,在全球拥有6万多名员工,遍布全球130个国家,并拥有覆盖全球11,500家企业的合作伙伴网 ...

  5. Codeforces 556 A Case of the Zeros and Ones

    A. Case of the Zeros and Ones time limit per test 1 second memory limit per test 256 megabytes input ...

  6. There are two sorted arrays nums1 and nums2 of size m and n respectively. Find the median of the two sorted arrays. The overall run time complexity should be O(log (m+n)).

    解题思路:合并两个数组,创建一个 Map对象,用以存放排好顺序的键值对,键为序号,值为数组值,中位数的结果分两种情况讨论: 1.m+n为奇数:(m+n)/2为中位数 2.m+n为偶数:(((m+n)/ ...

  7. swift UITapGestureRecognizer获取点击事件点击的位置point

    func picTap(sender: UITapGestureRecognizer) { let point = sender.location(in: sender.view) } 其中获取的po ...

  8. html2canvas页面截图图片不显示

    前两天在一个群里,有人问使用html2canvas屏幕截图的时候为什么页面的图片不显示只显示了文字,我没有做过屏幕截图的需求,所以不是很清楚,今天稍稍测试了一下. 在github上将html2canv ...

  9. 读书笔记 之《Thinking in Java》(对象、集合)

    一.前言: 本来想看完书再整理下自己的笔记的,可是书才看了一半发现笔记有点多,有点乱,就先整理一份吧,顺便复习下前面的知识,之后的再补上. 真的感觉,看书是个好习惯啊,难怪人家说"书籍是人类 ...

  10. 关于mac下配置mysql心得

    PS:配置一个mysql烦了一天,不过还是有所收获. 首先,下载安装我就不多啰嗦了.关键是在我们安装的最后会有一个临时密码,例如我的PBxsy=ES71(u: 这是非常重要的信息,如果没有得到的话,建 ...