概述
封装记录(Encapsulate Record)
曾用名:以数据类取代记录(Replace Record with Data Class)
organization = { name: "Acme Gooseberries", country: "GB" };
重构后
class Organization {
constructor(data) {
this._name = data.name;
this._country = data.country;
}
get name() { return this._name; }
set name(arg) { this._name = arg; }
get country() { return this._country; }
set country(arg) { this._country = arg; }
}
动机
记录型结构是多数编程语言提供的一种常见特性。它们能直观地组织起存在关联的数据,让我可以将数据作为有意义的单元传递,而不仅是一堆数据的拼凑。但简单的记录型结构也有缺陷,最恼人的一点是,它强迫我清晰地区分“记录中存储的数据”和“通过计算得到的数据”。假使我要描述一个整数闭区间,我可以用{start: 1, end: 5}描述,或者用{start: 1,length: 5}(甚至还能用{end: 5, length: 5},如果我想露两手华丽的编程技巧的话)。但不论如何存储,这3个值都是我想知道的,即区间的起点(start)和终点(end),以及区间的长度(length)。
这就是对于可变数据,我总是更偏爱使用类对象而非记录的原因。对象可以隐藏结构的细节,仅为这3个值提供对应的方法。该对象的用户不必追究存储的细节和计算的过程。同时,这种封装还有助于字段的改名:我可以重新命名字段,但同时提供新老字段名的访问方法,这样我就可以渐进地修改调用方,直到替换全部完成。
注意,我所说的偏爱对象,是对可变数据而言。如果数据不可变,我大可直接将这3个值保存在记录里,需要做数据变换时增加一个填充步骤即可。重命名记录也一样简单,你可以复制一个字段并逐步替换引用点。记录型结构可以有两种类型:一种需要声明合法的字段名字,另一种可以随便用任何字段名字。后者常由语言库本身实现,并通过类的形式提供出来,这些类称为散列(hash)、映射(map)、散列映射(hashmap)、字典(dictionary)或关联数组(associative array)等。很多编程语言都提供了方便的语法来创建这类记录,这使得它们在各种编程场景下都能大展身手。但使用这类结构也有缺陷,那就是一条记录上持有什么字段往往不够直观。比如说,如果我想知道记录里维护的字段究竟是起点/终点还是起点/长度,就只有查看它的创建点和使用点,除此以外别无他法。若这种记录只在程序的一个小范围里使用,那问题还不大,但若其使用范围变宽,“数据结构不直观”这个问题就会造成更多困扰。我可以重构它,使其变得更直观——但如果真需要这样做,那还不如使用类来得直接。
程序中间常常需要互相传递嵌套的列表(list)或散列映射结构,这些数据结构后续经常需要被序列化成JSON或XML。这样的嵌套结构同样值得封装,这样,如果后续其结构需要变更或者需要修改记录内的值,封装能够帮我更好地应对变化。
做法
- 对持有记录的变量使用封装变量(132),将其封装到一个函数中
记得为这个函数取一个容易搜索的名字。 - 创建一个类,将记录包装起来,并将记录变量的值替换为该类的一个实例。然后在类上定义一个访问函数,用于返回原始的记录。修改封装变量的函数,令其使用这个访问函数
- 测试
- 新建一个函数,让它返回该类的对象,而非那条原始的记录
- 对于该记录的每处使用点,将原先返回记录的函数调用替换为那个返回实例对象的函数调用。使用对象上的访问函数来获取数据的字段,如果该字段的访问函数还不存在,那就创建一个。每次更改之后运行测试
如果该记录比较复杂,例如是个嵌套解构,那么先重点关注客户端对数据的更新操作,对于读取操作可以考虑返回一个数据副本或只读的数据代理。 - 移除类对原始记录的访问函数,那个容易搜索的返回原始数据的函数也要一并删除
- 测试
- 如果记录中的字段本身也是复杂结构,考虑对其再次应用封装记录(162)或封装集合(170)手法
范例
首先,我从一个常量开始,该常量在程序中被大量使用。
const organization = { name: "Acme Gooseberries", country: "GB" };
这是一个普通的JavaScript对象,程序中很多地方都把它当作记录型结构在使用。以下是对其进行读取和更新的地方:
result += `<h1>${organization.name}</h1>`;
organization.name = newName;
重构的第一步很简单,先施展一下封装变量(132)。
function getRawDataOfOrganization() { return organization; }
读取的例子…
result += `<h1>${getRawDataOfOrganization().name}</h1>`;
更新的例子…
getRawDataOfOrganization().name = newName;
这里施展的不全是标准的封装变量(132)手法,我刻意为设值函数取了一个又丑又长、容易搜索的名字,因为我有意不让它在这次重构中活得太久。
封装记录意味着,仅仅替换变量还不够,我还想控制它的使用方式。我可以用类来替换记录,从而达到这一目的。
class Organization...
class Organization {
constructor(data) {
this._data = data;
}
}
顶层作用域
const organization = new Organization({ name: "Acme Gooseberries", country: "GB" });
function getRawDataOfOrganization() { return organization._data; }
function getOrganization() { return organization; }
创建完对象后,我就能开始寻找该记录的使用点了。所有更新记录的地方,用一个设值函数来替换它。
class Organization...
set name(aString) {this._data.name = aString;}
客户端…
result += `<h1>${getOrganization().name}</h1>`;
完成引用点的替换后,就可以兑现我之前的死亡威胁,为那个名称丑陋的函数送终了。
function getRawDataOfOrganization() { return organization._data; }
function getOrganization() { return organization; }
我还倾向于把_data里的字段展开到对象中。
class Organization {
constructor(data) {
this._name = data.name;
this._country = data.country;
}
get name() { return this._name; }
set name(aString) { this._name = aString; }
get country() { return this._country; }
set country(aCountryCode) { this._country = aCountryCode; }
}
这样做有一个好处,能够使外界无须再引用原始的数据记录。直接持有原始的记录会破坏封装的完整性。但有时也可能不适合将对象展开到独立的字段里,此时我就会先将_data复制一份,再进行赋值。
范例:封装嵌套记录
上面的例子将记录的浅复制展开到了对象里,但当我处理深层嵌套的数据(比如来自JSON文件的数据)时,又该怎么办呢?此时该重构手法的核心步骤依然适用,记录的更新点需要同样小心处理,但对记录的读取点则有多种处理方案。
作为例子,这里有一个嵌套层级更深的数据:它是一组顾客信息的集合,保存在散列映射中,并通过顾客ID进行索引。
对嵌套数据的更新和读取可以进到更深的层级。
更新的例子…
customerData[customerID].usages[year][month] = amount;
读取的例子…
function compareUsage(customerID, laterYear, month) {
const later = customerData[customerID].usages[laterYear][month];
const earlier = customerData[customerID].usages[laterYear - 1][month];
return { laterAmount: later, change: later - earlier };
}
对这样的数据施行封装,第一步仍是封装变量(132)。
function getRawDataOfCustomers() { return customerData; }
function setRawDataOfCustomers(arg) { customerData = arg; }
更新的例子…
getRawDataOfCustomers()[customerID].usages[year][month] = amount;
读取的例子…
function compareUsage(customerID, laterYear, month) {
const later = getRawDataOfCustomers()[customerID].usages[laterYear][month];
const earlier = getRawDataOfCustomers()[customerID].usages[laterYear - 1][month];
return { laterAmount: later, change: later - earlier };
}
接下来我要创建一个类来容纳整个数据结构。
class CustomerData {
constructor(data) {
this._data = data;
}
}
顶层作用域…
function getCustomerData() { return customerData; }
function getRawDataOfCustomers() { return customerData._data; }
function setRawDataOfCustomers(arg) { customerData = new CustomerData(arg); }
最重要的是妥善处理好那些更新操作。因此,当我查看getRawDataOfCustomers的所有调用者时,总是特别关注那些对数据做修改的地方。再提醒你一下,下面是那步更新操作。
更新的例子…
getRawDataOfCustomers()[customerID].usages[year][month] = amount;
“做法”部分说,接下来要通过一个访问函数来返回原始的顾客数据,如果访问函数还不存在就创建一个。现在顾客类还没有设值函数,而且这个更新操作对结构进行了深入查找,因此是时候创建一个设值函数了。我会先用提炼函数(106),将层层深入数据结构的查找操作提炼到函数里。
更新的例子…
setUsage(customerID, year, month, amount);
顶层作用域…
function setUsage(customerID, year, month, amount) {
getRawDataOfCustomers()[customerID].usages[year][month] = amount;
}
然后我再用搬移函数(198)将新函数搬移到新的顾客数据类中。
更新的例子…
getCustomerData().setUsage(customerID, year, month, amount);
class CustomerData...
setUsage(customerID, year, month, amount) {
this._data[customerID].usages[year][month] = amount;
}
封装大型的数据结构时,我会更多关注更新操作。凸显更新操作,并将它们集中到一处地方,是此次封装过程最重要的一部分。
一通替换过后,我可能认为修改已经告一段落,但如何确认替换是否真正完成了呢?检查的办法有很多,比如可以修改getRawDataOfCustomers函数,让其返回一份数据的深复制的副本。如果测试覆盖足够全面,那么当我真的遗漏了一些更新点时,测试就会报错。
顶层作用域…
function getCustomerData() { return customerData; }
function getRawDataOfCustomers() { return customerData.rawData; }
function setRawDataOfCustomers(arg) { customerData = new CustomerData(arg); }
class CustomerData...
get rawData() {
return _.cloneDeep(this._data);
}
我使用了lodash库来辅助生成深复制的副本。
另一个方式是,返回一份只读的数据代理。如果客户端代码尝试修改对象的结构,那么该数据代理就会抛出异常。这在有些编程语言中能轻易实现,但用JavaScript实现可就麻烦了,我把它留给读者作为练习好了。或者,我可以复制一份数据,递归冻结副本的每个字段,以此阻止对它的任何修改企图。
妥善处理好数据的更新当然价值不凡,但读取操作又怎么处理呢?这有几种选择。
第一种选择是与设值函数采用同等待遇,把所有对数据的读取提炼成函数,并将它们搬移到CustomerData类中。
class CustomerData...
usage(customerID, year, month) {
return this._data[customerID].usages[year][month];
}
顶层作用域…
function compareUsage(customerID, laterYear, month) {
const later = getCustomerData().usage(customerID, laterYear, month);
const earlier = getCustomerData().usage(customerID, laterYear - 1, month);
return { laterAmount: later, change: later - earlier };
}
这种处理方式的美妙之处在于,它为customerData提供了一份清晰的API列表,清楚描绘了该类的全部用途。我只需阅读类的代码,就能知道数据的所有用法。但这样会使代码量剧增,特别是当对象有许多用途时。现代编程语言大多提供直观的语法,以支持从深层的列表和散列[mf-lh]结构中获得数据,因此直接把这样的数据结构给到客户端,也不失为一种选择。
如果客户端想拿到一份数据结构,我大可以直接将实际的数据交出去。但这样做的问题在于,我将无从阻止用户直接对数据进行修改,进而使我们封装所有更新操作的良苦用心失去意义。最简单的应对办法是返回原始数据的一份副本,这可以用到我前面写的rawData方法。
class CustomerData...
get rawData() {
return _.cloneDeep(this._data);
}
顶层作用域…
function compareUsage(customerID, laterYear, month) {
const later = getCustomerData().rawData[customerID].usages[laterYear][month];
const earlier = getCustomerData().rawData[customerID].usages[laterYear - 1][month];
return { laterAmount: later, change: later - earlier };
}
简单归简单,这种方案也有缺点。最明显的问题是复制巨大的数据结构时代价颇高,这可能引发性能问题。不过也正如我对性能问题的一贯态度,这样的性能损耗也许是可以接受的——只有测量到可见的影响,我才会真的关心它。这种方案还可能带来困惑,比如客户端可能期望对该数据的修改会同时反映到原数据上。如果采用了只读代理或冻结副本数据的方案,就可以在此时提供一个有意义的错误信息。
另一种方案需要更多工作,但能提供更可靠的控制粒度:对每个字段循环应用封装记录。我会把顾客(customer)记录变成一个类,对其用途(usage)字段应用封装集合(170),并为它创建一个类。然后我就能通过访问函数来控制其更新点,比如说对用途(usage)对象应用将引用对象改为值对象(252)。但处理一个大型的数据结构时,这种方案异常繁复,如果对该数据结构的更新点没那么多,其实大可不必这么做。有时,合理混用取值函数和新对象可能更明智,即使用取值函数来封装数据的深层查找操作,但更新数据时则用对象来包装其结构,而非直接操作未经封装的数据。我在“Refactoring Code to Load a Document”[mf-ref-doc]这篇文章中讨论了更多的细节,有兴趣的读者可移步阅读。
封装集合(Encapsulate Collection)
class Person {
get courses() {return this._courses;}
set courses(aList) {this._courses = aList;}
}
重构后
class Person {
get courses() { return this._courses.slice(); }
addCourse(aCourse) { ... }
removeCourse(aCourse) { ... }
}
动机
我喜欢封装程序中的所有可变数据。这使我很容易看清楚数据被修改的地点和修改方式,这样当我需要更改数据结构时就非常方便。我们通常鼓励封装——使用面向对象技术的开发者对封装尤为重视——但封装集合时人们常常犯一个错误:只对集合变量的访问进行了封装,但依然让取值函数返回集合本身。这使得集合的成员变量可以直接被修改,而封装它的类则全然不知,无法介入。
为避免此种情况,我会在类上提供一些修改集合的方法——通常是“添加”和“移除”方法。这样就可使对集合的修改必须经过类,当程序演化变大时,我依然能轻易找出修改点。
只要团队拥有良好的习惯,就不会在模块以外修改集合,仅仅提供这些修改方法似乎也就足够。然而,依赖于别人的好习惯是不明智的,一个细小的疏忽就可能带来难以调试的bug。更好的做法是,不要让集合的取值函数返回原始集合,这就避免了客户端的意外修改。
一种避免直接修改集合的方法是,永远不直接返回集合的值。这种方法提倡,不要直接使用集合的字段,而是通过定义类上的方法来代替,比如将aCustomer.orders.size替换为aCustomer.numberOfOrders。我不同意这种做法。现代编程语言都提供了丰富的集合类和标准接口,能够组合成很多有价值的用法,比如集合管道(CollectionPipeline)[mf-cp]等。使用特殊的类方法来处理这些场景,会增加许多额外代码,使集合操作容易组合的特性大打折扣。
还有一种方法是,以某种形式限制集合的访问权,只允许对集合进行读操作。比如,在Java中可以很容易地返回集合的一个只读代理,这种代理允许用户读取集合,但会阻止所有更改操作——Java的代理会抛出一个异常。有一些库在构造集合时也用了类似的方法,将构造出的集合建立在迭代器或枚举对象的基础上,因为迭代器也不能修改它迭代的集合。
也许最常见的做法是,为集合提供一个取值函数,但令其返回一个集合的副本。这样即使有人修改了副本,被封装的集合也不会受到影响。这可能带来一些困惑,特别是对那些已经习惯于通过修改返回值来修改原集合的开发者——但更多的情况下,开发者已经习惯于取值函数返回副本的做法。如果集合很大,这个做法可能带来性能问题,好在多数列表都没有那么大,此时前述的性能优化基本守则依然适用(见“重构与性能”那节)。
使用数据代理和数据复制的另一个区别是,对源数据的修改会反映到代理上,但不会反映到副本上。大多数时候这个区别影响不大,因为通过此种方式访问的列表通常生命周期都不长。
采用哪种方法并无定式,最重要的是在同个代码库中做法要保持一致。我建议只用一种方案,这样每个人都能很快习惯它,并在每次调用集合的访问函数时期望相同的行为。
做法
- 如果集合的引用尚未被封装起来,先用封装变量(132)封装它
- 在类上添加用于“添加集合元素”和“移除集合元素”的函数
如果存在对该集合的设值函数,尽可能先用移除设值函数(331)移除它。如果不能移除该设值函数,至少让它返回集合的一份副本。 - 执行静态检查
- 查找集合的引用点。如果有调用者直接修改集合,令该处调用使用新的添加/移除元素的函数。每次修改后执行测试
- 修改集合的取值函数,使其返回一份只读的数据,可以使用只读代理或数据副本
- 测试
范例
假设有个人(Person)要去上课。我们用一个简单的Course来表示“课程”。
class Person...
class Person {
constructor(name) {
this._name = name;
this._courses = [];
}
get name() { return this._name; }
get courses() { return this._courses; }
set courses(aList) { this._courses = aList; }
}
class Course...
class Course {
constructor(name, isAdvanced) {
this._name = name;
this._isAdvanced = isAdvanced;
}
get name() { return this._name; }
get isAdvanced() { return this._isAdvanced; }
}
客户端会使用课程集合来获取课程的相关信息。
numAdvancedCourses = aPerson.courses.filter(c => c.isAdvanced).length;
有些开发者可能觉得这个类已经得到了恰当的封装,毕竟,所有的字段都被访问函数保护到了。但我要指出,对课程列表的封装还不完整。诚然,对列表整体的任何更新操作,都能通过设值函数得到控制。
客户端代码…
const basicCourseNames = readBasicCourseNames(filename);
aPerson.courses = basicCourseNames.map(name => new Course(name, false));
但客户端也可能发现,直接更新课程列表显然更容易。
客户端代码…
for (const name of readBasicCourseNames(filename)) {
aPerson.courses.push(new Course(name, false));
}
这就破坏了封装性,因为以此种方式更新列表Person类根本无从得知。这里仅仅封装了字段引用,而未真正封装字段的内容。
现在我来对类实施真正恰当的封装,首先要为类添加两个方法,为客户端提供“添加课程”和“移除课程”的接口。
class Person...
addCourse(aCourse) {
this._courses.push(aCourse);
}
removeCourse(aCourse, fnIfAbsent = () => { throw new RangeError(); }) {
const index = this._courses.indexOf(aCourse);
if (index === -1) fnIfAbsent();
else this._courses.splice(index, 1);
}
对于移除操作,我得考虑一下,如果客户端要求移除一个不存在的集合元素怎么办。我可以耸耸肩装作没看见,也可以抛出错误。这里我默认让它抛出错误,但留给客户端一个自己处理的机会。
然后我就可以让直接修改集合值的地方改用新的方法了。
客户端代码…
for (const name of readBasicCourseNames(filename)) {
aPerson.addCourse(new Course(name, false));
}
有了单独的添加和移除方法,通常setCourse设值函数就没必要存在了。若果真如此,我就会使用移除设值函数(331)移除它。如果出于其他原因,必须提供一个设值方法作为API,我至少要确保用一份副本给字段赋值,不去修改通过参数传入的集合。
class Person...
set courses(aList) { this._courses = aList.slice(); }
这套设施让客户端能够使用正确的修改方法,同时我还希望能确保所有修改都通过这些方法进行。为达此目的,我会让取值函数返回一份副本。
class Person...
get courses() {return this._courses.slice();}
总的来讲,我觉得对集合保持适度的审慎是有益的,我宁愿多复制一份数据,也不愿去调试因意外修改集合招致的错误。修改操作并不总是显而易见的,比如,在JavaScript中原生的数组排序函数sort()就会修改原数组,而在其他语言中默认都是为更改集合的操作返回一份副本。任何负责管理集合的类都应该总是返回数据副本,但我还养成了一个习惯,只要我做的事看起来可能改变集合,我也会返回一个副本。
以对象取代基本类型(Replace Primitive with Object)
曾用名:以对象取代数据值(Replace Data Value with Object)
曾用名:以类取代类型码(Replace Type Code with Class)
orders.filter(o => "high" === o.priority || "rush" === o.priority);
重构后
orders.filter(o => o.priority.higherThan(new Priority("normal")));
动机
开发初期,你往往决定以简单的数据项表示简单的情况,比如使用数字或字符串等。但随着开发的进行,你可能会发现,这些简单数据项不再那么简单了。比如说,一开始你可能会用一个字符串来表示“电话号码”的概念,但是随后它又需要“格式化”“抽取区号”之类的特殊行为。这类逻辑很快便会占领代码库,制造出许多重复代码,增加使用时的成本。
一旦我发现对某个数据的操作不仅仅局限于打印时,我就会为它创建一个新类。一开始这个类也许只是简单包装一下简单类型的数据,不过只要类有了,日后添加的业务逻辑就有地可去了。这些小小的封装值开始可能价值甚微,但只要悉心照料,它们很快便能成长为有用的工具。创建新类无须太大的工作量,但我发现它们往往对代码库有深远的影响。实际上,许多经验丰富的开发者认为,这是他们的工具箱里最实用的重构手法之一——尽管其价值常为新手程序员所低估。
做法
- 如果变量尚未被封装起来,先使用封装变量(132)封装它
- 为这个数据值创建一个简单的类。类的构造函数应该保存这个数据值,并为它提供一个取值函数
- 执行静态检查
- 修改第一步得到的设值函数,令其创建一个新类的对象并将其存入字段,如果有必要的话,同时修改字段的类型声明
- 修改取值函数,令其调用新类的取值函数,并返回结果
- 测试
- 考虑对第一步得到的访问函数使用函数改名(124),以便更好反映其用途
- 考虑应用将引用对象改为值对象(252)或将值对象改为引用对象(256),明确指出新对象的角色是值对象还是引用对象
范例
我将从一个简单的订单(Order)类开始。该类从一个简单的记录结构里读取所需的数据,这其中有一个订单优先级(priority)字段,它是以字符串的形式被读入的。
class Order...
class Order {
constructor(data) {
this.priority = data.priority;
// more initialization
}
}
客户端代码有些地方是这么用它的:
客户端…
highPriorityCount = orders.filter(o => "high" === o.priority || "rush" === o.priority).length;
无论何时,当我与一个数据值打交道时,第一件事一定是对它使用封装变量(132)。
class Order...
get priority() { return this._priority; }
set priority(aString) { this._priority = aString; }
现在构造函数中第一行初始化代码就会使用我刚刚创建的设值函数了。
这使它成了一个自封装的字段,因此我暂可放任原来的引用点不理,先对字段进行处理。
接下来我为优先级字段创建一个简单的值类(value class)。该类应该有一个构造函数接收值字段,并提供一个返回字符串的转换函数。
class Priority {
constructor(value) { this._value = value; }
toString() { return this._value; }
}
这里的转换函数我更倾向于使用toString而不用取值函数(value)。对类的客户端而言,一个返回字符串描述的API应该更能传达“发生了数据转换”的信息,而使用取值函数取用一个字段就缺乏这方面的感觉。
然后我要修改访问函数,使其用上新创建的类。
class Order...
get priority() { return this._priority.toString(); }
set priority(aString) { this._priority = new Priority(aString); }
提炼出Priority类后,我发觉现在Order类上的取值函数命名有点儿误导人了。它确实还是返回了优先级信息,但却是一个字符串描述,而不是一个Priority对象。于是我立即对它应用了函数改名(124)。
class Order...
get priorityString() {return this._priority.toString();}
set priority(aString) {this._priority = new Priority(aString);}
客户端…
highPriorityCount = orders.filter(o => "high" === o.priorityString || "rush" === o.priorityString).length;
这里设值函数的名字倒没有使我不满,因为函数的参数能够清晰地表达其意图。
到此为止,正式的重构手法就结束了。不过当我进一步查看优先级字段的客户端时,我在想让它们直接使用Priority对象是否会更好。于是,我着手在订单类上添加一个取值函数,让它直接返回新建的Priority对象。
class Order...
get priority() { return this._priority; }
get priorityString() { return this._priority.toString(); }
set priority(aString) { this._priority = new Priority(aString); }
客户端…
highPriorityCount = orders.filter(o => "high" === o.priority.toString() || "rush" === o.priority.toString()).length;
随着Priority对象在别处也有了用处,我开始支持让Order类的客户端拿着Priority实例来调用设值函数,这可以通过调整Priority类的构造函数实现。
class Priority...
constructor(value) {
if (value instanceof Priority) return value;
this._value = value;
}
这样做的意义在于,现在新的Priority类可以容纳更多业务行为——无论是新的业务代码,还是从别处搬移过来的。这里有些例子,它会校验优先级的传入值,支持一些比较逻辑。
class Priority...
class Priority {
constructor(value) {
if (value instanceof Priority) return value;
if (Priority.legalValues().includes(value)) {
this._value = value;
} else {
throw new Error(`<${value}> is invalid for Priority`);
}
}
toString() { return this._value; }
get _index() { return Priority.legalValues().findIndex(s => s === this._value); }
static legalValues() {
return ['low', 'normal', 'high', 'rush'];
}
equals(other) { return this._index === other._index; }
higherThan(other) { return this._index > other._index; }
lowerThan(other) { return this._index < other._index; }
}
修改的过程中,我发觉它实际上已经担负起值对象(value object)的角色,因此我又为它添加了一个equals方法,并确保它的值不可修改。
加上这些行为后,我可以让客户端代码读起来含义更清晰。
客户端…
highPriorityCount = orders.filter(o => o.priority.higherThan(new Priority("normal"))).length;
最后
以上就是朴素奇异果为你收集整理的【重构篇】第六章 封装(第一部分)的全部内容,希望文章能够帮你解决【重构篇】第六章 封装(第一部分)所遇到的程序开发问题。
如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。
发表评论 取消回复