ThinkChat🤖让你学习和工作更高效,注册即送10W Token,即刻开启你的AI之旅 广告
反向重构:将值对象改为引用对象(256) ![](https://box.kancloud.cn/ccfb07485046dc1b46f5e51325e93579_403x362.jpeg) ``` class Product { applyDiscount(arg) {this._price.amount -= arg;} ``` ![](https://box.kancloud.cn/a3bed334e2e1f6d1a46c5039deb25af9_91x152.jpeg) ``` class Product { applyDiscount(arg) { this._price = new Money(this._price.amount - arg, this._price.currency); } ``` ### 动机 在把一个对象(或数据结构)嵌入另一个对象时,位于内部的这个对象可以被视为引用对象,也可以被视为值对象。两者最明显的差异在于如何更新内部对象的属性:如果将内部对象视为引用对象,在更新其属性时,我会保留原对象不动,更新内部对象的属性;如果将其视为值对象,我就会替换整个内部对象,新换上的对象会有我想要的属性值。 如果把一个字段视为值对象,我可以把内部对象的类也变成值对象\[mf-vo\]。值对象通常更容易理解,主要因为它们是不可变的。一般说来,不可变的数据结构处理起来更容易。我可以放心地把不可变的数据值传给程序的其他部分,而不必担心对象中包装的数据被偷偷修改。我可以在程序各处复制值对象,而不必操心维护内存链接。值对象在分布式系统和并发系统中尤为有用。 值对象和引用对象的区别也告诉我,何时不应该使用本重构手法。如果我想在几个对象之间共享一个对象,以便几个对象都能看见对共享对象的修改,那么这个共享的对象就应该是引用。 ### 做法 - 检查重构目标是否为不可变对象,或者是否可修改为不可变对象。 - 用移除设值函数(331)逐一去掉所有设值函数。 - 提供一个基于值的相等性判断函数,在其中使用值对象的字段。 > 大多数编程语言都提供了可覆写的相等性判断函数。通常你还必须同时覆写生成散列码的函数。 ### 范例 设想一个代表“人”的`Person`类,其中包含一个代表“电话号码”的`Telephone Number`对象。 ##### class Person... constructor() { ``` constructor() {  this._telephoneNumber = new TelephoneNumber(); } get officeAreaCode()  {return this._telephoneNumber.areaCode;} set officeAreaCode(arg) {this._telephoneNumber.areaCode = arg;} get officeNumber()  {return this._telephoneNumber.number;} set officeNumber(arg) {this._telephoneNumber.number = arg;} ``` ##### class TelephoneNumber... ``` get areaCode() {return this._areaCode;} set areaCode(arg) {this._areaCode = arg;} get number() {return this._number;} set number(arg) {this._number = arg;} ``` 代码的当前状态是提炼类(182)留下的结果:从前拥有电话号码信息的`Person`类仍然有一些函数在修改新对象的属性。趁着还只有一个指向新类的引用,现在是时候使用将引用对象改为值对象将其变成值对象。 我需要做的第一件事是把`TelephoneNumber`类变成不可变的。对它的字段运用移除设值函数(331)。移除设值函数(331)的第一步是,用改变函数声明(124)把这两个字段的初始值加到构造函数中,并迫使构造函数调用设值函数。 ##### class TelephoneNumber... ``` constructor(areaCode, number) { this._areaCode = areaCode; this._number = number; } ``` 然后我会逐一查看设值函数的调用者,并将其改为重新赋值整个对象。先从“地区代码”(area code)开始。 ##### class Person... ``` get officeAreaCode() {return this._telephoneNumber.areaCode;} set officeAreaCode(arg) {  this._telephoneNumber = new TelephoneNumber(arg, this.officeNumber); } get officeNumber()  {return this._telephoneNumber.number;} set officeNumber(arg) {this._telephoneNumber.number = arg;} ``` 对于其他字段,重复上述步骤。 ##### class Person... ``` get officeAreaCode() {return this._telephoneNumber.areaCode;} set officeAreaCode(arg) { this._telephoneNumber = new TelephoneNumber(arg, this.officeNumber); } get officeNumber() {return this._telephoneNumber.number;} set officeNumber(arg) {  this._telephoneNumber = new TelephoneNumber(this.officeAreaCode, arg); } ``` 现在,`TelephoneNumber`已经是不可变的类,可以将其变成真正的值对象了。是不是真正的值对象,要看是否基于值判断相等性。在这个领域中,JavaScript做得不好:语言和核心库都不支持将“基于引用的相等性判断”换成“基于值的相等性判断”。我唯一能做的就是创建自己的`equals`函数。 ##### class TelephoneNumber... ``` equals(other) {  if (!(other instanceof TelephoneNumber)) return false;  return this.areaCode === other.areaCode &&   this.number === other.number; } ``` 对其进行测试很重要: ``` it('telephone equals', function() {  assert(    new TelephoneNumber("312", "555-0142")      .equals(new TelephoneNumber("312", "555-0142"))); }); ``` 这段测试代码用了不寻常的格式,是为了帮助读者一眼看出上下两次构造函数调用完全一样。 我在这个测试中创建了两个各自独立的对象,并验证它们相等。 > 在大多数面向对象语言中,内置的相等性判断方法可以被覆写为基于值的相等性判断。在Ruby中,我可以覆写`==`运算符;在Java中,我可以覆写`Object.equals()`方法。在覆写相等性判断的同时,我通常还需要覆写生成散列码的方法(例如Java中的`Object.hashCode()`方法),以确保用到散列码的集合在使用值对象时一切正常。 如果有多个客户端使用了`TelephoneNumber`对象,重构的过程还是一样,只是在运用移除设值函数(331)时要修改多处客户端代码。另外,有必要添加几个测试,检查电话号码不相等以及与非电话号码和`null`值比较相等性等情况。