本章将介绍几个能让你更轻松处理数据的重构手法。很多人或许会认为Self Encapsulate Fild(171)有点多余,但是关于"对象应该直接访问其中的数据,抑或应该通过访问函数来访问"这一问题,争论的声音从来不曾停止。有时候你确实需要访问函数,此时就可以通过SelfEncapsulate Field(171)得到它们。通常我会选择"直接访问"方式,因为我发现,只要我想做,任何时候进行这项重构都是很简单的。
面向对象语言有一个很有用的特征∶除了允许使用传统语言提供的简单数据类型,它们还允许你定义新类型。不过人们往往需要一段时间才能习惯这种编程方式。一开始你常会使用一个简单数值来表示某个概念。随着对系统的深入了解,你可能会明白,以对象表示这个概念,可能更合适。Replace Valuewth Object(175)让你可以将"哑"数据变成善表达的对象。如果你发现程序中有太多地方需要这一类对象,也可以使用Change Value to Reference(179)将它们变成引用对象。
如果你看到一个数组的行为方式很像一个数据结构,就可以使用Replace Array withObject(186)把数组变成对象,从而使这个数据结构更清晰地显露出来。但这只是第一步,当你使用Move Method(142)为这个新对象加入相应行为时,真正的好处才得以体现。
魔法数———也就是带有特殊含义的数字———从来都是个问题。我还清楚记得,一开始学习编程的时候,老师就告诉我不要使用魔法数。但它们还是不时出现。因此,只要弄清楚魔法数的用途,我就运用 ReplaceMagicNumberwith Symbolic Constant (204)将它们除掉,以绝后患。
对象之间的关联可以是单向的,也可以是双向的。单向关联比较简单,但有时为了支持一项新功能,你需要使用Change UnidirectionalAssociation to Bidirectional (197))将它变成双向关联。Change BidirecionalAssociation to Unidirectional(200)则恰恰相反∶如果你发现不再需要双向关联,可以使用这项重构将它变成单向关联。
我常常遇到这样的情况∶GUI类竟然去处理不该它们处理的业务逻辑。为了把这些处理业务逻辑的行为移到合适的领域类去,你需要在领域类中保存这些逻辑的相关数据,并运用Duplicate ObservedData(189)提供对GUI的支持。一般来说,我不喜欢重复的数据,但这是一个例外,因为这里的重复数据通常是不可避免的。
面向对象编程的关键原则之一就是封装。如果一个类公开了任何public数据,你就应该使用Encapsulate Field(206)将它郑重地包装起来。如果被公开的数据是个集合,就应该使用Encapsulate Collection(208),因为集合有其特殊协议。如果一整条记录都被裸露在外,就应该使用Replace Recordwith Data Class(217)。
需要特别对待的一种数据是类型码(type code)∶这是一种特殊数值,用来指出"与实例所属之类型相关的某些东西"。类型码通常以枚举形式出现,并且通常以staticfinal整数实现。如果这些类型码用来表现某种信息,并且不会改变所属类型的行为,你可以运用Replace Type Code with Class((218)将它们替换掉,这项重构会为你提供更好的类型检查,以及一个更好的平台,使你可以在未来更方便地将相关行为添加进去。另一方面,如果当前类型的行为受到类型码的影响,你就应该尽可能使用ReplaceType CodewithSubclas(223)。如果做不到,就只好使用更复杂(同时也更灵活)的Replace Type Code with State/Srategy(227)。
8.1 Self EncapsulateField(自封装字段)
你直接访问一个字段,但与字段之间的耦合关系逐渐变得笨拙
为了这个字段建立取值/设值函数,并且只以这些函数来访问字段.
动机
间接好处是,数据管理更加灵活,例如延迟初始化.
直接访问变量的好处则是:代码比较容易阅读.
做法
- 为待封装字段建立取值/设值函数
- 找出该字段的所有引用点,将他们全部改为调用取值/设值函数.
- 将该字段声明为private
- 复查,确保找出所有引用点.
- 编译,测试
8.2 Replace Data Value with Object(以对象取代数据值)
你有一个数据项,需要与其他数据和行为一起使用才有意义
将数据变成对象.
动机
开发初期,你往往决定以简单的数据项表示简单的情况。但是,随着开发的进行,你可能会发现,这些简单数据项不再那么简单了。比如说,一开始你可能会用一个字符串来表示"电话号码"概念,但是随后你就会发现,电话号码需要"格式化"、"抽取区号"之类的特殊行为。如果这样的数据项只有一两个,你还可以把相关函数放进数据项所属的对象里;但是Duplicate Code坏味道和Feature Envy坏味道很快就会从代码中散发出来。当这些坏味道开始出现,你就应该将数据值变成对象。
做法
- 为待替换数值新建一个类,在其中声明一个final字段,其类型和源类中的带替换数值类型一样.然后在新类中加入这个字段的取值函数,再加上一个接受此字段为参数的构造函数.
- 编译
- 将源类中的待替换数值字段的类型改为前面新建的类.
- 修改源类中该字段的取值函数,令它调用新类的取值函数
- 如果源类构造函数中用到这个待替换字段(多半是赋值动作),我们就修改构造函数,令它改用新类的构造函数来对字段进行赋值动作.
- 修改源类中待替换字段的设值函数,令它为新类创建一个实例.
- 编译,测试.
- 现在,你有可能需要对新类型使用Change Value to Reference(179).
8.3 Change Value to Reference(将值对象改为引用对象)
你从一个类衍生出许多彼此相等的实例,希望将他们替换为同一个对象.
将这个值对象变成引用对象.
动机
在许多系统中,你都可以对对象做一个有用的分类∶引用对象和值对象。前者就像"客户"、"账户"这样的东西,每个对象都代表真实世界中的一个实物,你可以直接以相等操作符(=,用来检验对象同一性)检查两个对象是否相等。后者则是像"日期"、"钱"这样的东西,它们完全由其所含的数据值来定义,你并不在意副本的存在,系统中或许存在成百上千个内容为"1/1/2000"的"日期"对象。当然,你也需要知道两个值对象是否相等,所以你需要覆写equal1s()(以及hashCode()。
要在引用对象和值对象之间做选择有时并不容易。有时候,你会从一个简单的值对象开始,在其中保存少量不可修改的数据。而后,你可能会希望给这个对象加入一些可修改数据,并确保对任何一个对象的修改都能影响到所有引用此一对象的地方。这时候你就需要将这个对象变成一个引用对象。
做法
- 使用Replace Constructor with Factory Method(304).
- 编译,测试
- 决定由什么对象负责提供访问对象的途径.
- 决定这些引用对象应该预先创建好, 或是应该动态创建
- 修改工厂函数,令它返回引用对象.
- 编译,测试.
8.4 Change Reference to Value (将引用对象改为值对象)
你有一个引用对象,很小且不可变,且不易管理.
将他变成一个值对象.
动机
正如我在Change Value to Reference(179)中所说,要在引用对象和值对象之间做选择,有时并不容易。作出选择后,你常会需要一条回头路。
如果引用对象开始变得难以使用,也许就应该将它改为值对象。引用对象必须被某种方式控制,你总是必须向其控制者请求适当的引用对象。它们可能造成内存区域之间错综复杂的关联。在分布系统和并发系统中,不可变的值对象特别有用,因为你无需考虑它们的同步问题。
值对象有一个非常重要的特性∶它们应该是不可变的。无论何时,只要你调用同一对象的同一个查询函数,都应该得到同样结果。如果保证了这一点,就可以放心地以多个对象表示同一个事物。如果值对象是可变的,你就必须确保对某一对象的修改会自动更新其他"代表相同事物"的对象。这太痛苦了,与其如此还不如把它变成引用对象。
这里有必要澄清一下"不可变"(immutable)的意思。如果你以Money类表示"钱"的概念,其中有"币种"和"金额"两条信息,那么Money对象通常是一个不可变的值对象。这并非意味你的薪资不能改变,而是意味∶如果要改变你的薪资,就需要使用另一个Money对象来取代现有的Money对象,而不是在现有的Money对象上修改。你和Money对象之间的关系可以改变,但Money对象自身不能改变。
做法
- 检查重构目标是否为不可变对象,或是否可修改为不可变对象.
- 建立equals()和hashCode()
- 编译,测试.
- 考虑是否可以删除工厂函数,并将构造函数声明为public
8.5 Replace Array with Object(以对象取代数组)
你有一个数组,其中的元素各自代表不同的东西.
以对象替换数组.对于数组中的每个元素,以一个字段来表示.
动机
数组是一种常见的用以组织数据的结构。不过,它们应该只用于"以某种顺序容纳一组相似对象"。有时候你会发现,一个数组容纳了多种不同对象,这会给用户带来麻烦,因为他们很难记住像"数组的第一个元素是人名"这样的约定。对象就不同了,你可以运用字段名称和函数名称来传达这样的信息,因此你无需死记它,也无需依赖注释。而且如果使用对象,你还可以将信息封装起来,并使用Move Method (142)为它加上相关行为。
做法
- 新建一个累标识数组所拥有的信息,并在其中以一个public 字段保存原先的数组。
- 修改数组的所有用户,让他们改用新类的实例
- 编译,测试
- 逐一为数组元素添加取值/设置函数。根据元素的用途,为这些访问函数命名。修改客户端代码,让他们通过访问函数取用数组内的元素,每次修改后,编译并测试
- 当所有对数组的直接访问都转而调用访问函数后,将新类中保存该数组的字段声明为private.
- 编译
- 对于数组内的每一个元素,在新类中创建一个类型相当的字段。修改该元素的访问函数,令它改用上述的新建字段
- 每修改一个元素,编译并测试。
- 数组的所有元素都有了响应字段之后,删除该数组
8.6 Duplicate Observed Data(复制"被监视数据")
你有一些领域数据置身于GUI控件中,而领域函数需要访问这些数据。
将该数据复制到一个领域对象中。建立一个Observer模式,用以同步领域对象和GUI对象内的重复数据。
动机
一个分层良好的系统,应该将处理用户界面和处理业务逻辑的代码分开。之所以这样做,原因有以下几点∶(1)你可能需要使用不同的用户界面来表现相同的业务逻辑,如果同时承担两种责任,用户界面会变得过分复杂;(2)与GUI隔离之后,领域对象的维护和演化都会更容易,你甚至可以让不同的开发者负责不同部分的开发。
尽管可以轻松地将"行为"划分到不同部位,"数据"却往往不能如此。同一项数据有可能既需要内嵌于GUI控件,也需要保存于领域模型里。自从MVC (Model-View-Controller,模型-视图-控制器)模式出现后,用户界面框架都使用多层系统来提供某种机制,使你不但可以提供这类数据,并保持它们同步。
如果你遇到的代码是以两层方式开发,业务逻辑被内嵌于用户界面之中,你就有必要将行为分离出来。其中的主要工作就是函数的分解和搬移。但数据就不同了∶你不能仅仅只是移动数据,必须将它复制到新的对象中,并提供相应的同步机制。
做法
- 修改展现类,使其成为领域类的Observer
- 针对GUI类中的领域数据,使用SelfEncapsulate Field(171)
- 编译,测试
- 在时间处理函数中调用设值函数,直接更新GUI组件
- 编译,测试。
- 在领域类中定义数据及其相关访问函数。
- 修改展现类中的访问函数,将他们的操作对象改为领域对象(而非GUI组件)
- 修改Observer的Update(),使其从相应的领域对象中将所需数据复制给GUI组件。
- 编译,测试
使用事件监听器
如果你使用事件监听器而不是Observer/Observable模式,仍然可以实施Duplicate ObservedData(189)。这种情况下,你需要在领域模型中建立一个监听器类和一个事件类(如果你不在意依赖关系的话,也可以使用AWT类)。然后,你需要对领域对象注册监听器,就像前例对observable对象注册observer一样。每当领域对象发生变化(类似上例的update()函数被调用),就向监听器发送一个事件。IntervalWindow可以利用一个内嵌类来实现监听器接口,并在适当时候调用适当的update()函数。
8.7 Change Unidirectional Association to Bidirectional(将单向关联改为双向关联)
两个类都需要事用对方特性,但其间只有一条单向连接。
添加一个反向指针,并使修改函数能够同时更新两条连接
动机
开发初期,你可能会在两个类之间建立一条单向连接,使其中一个类可以引用另一个类。随着时间推移,你可能发现被引用类需要得到其引用者以便进行某些处理。也就是说它需要一个反向指针。但指针是一种单向连接,你不可能反向操作它。通常你可以绕道而行,虽然会耗费一些计算时间,成本还算合理,然后你可以在被引用类中建立一个函数专门负责此一行为。但是,有时候想绕过这个问题并不容易,此时就需要建立双向引用关系,或称为反向指针。如果使用不当,反向指针很容易造成混乱;但只要你习惯了这种手法,它们其实并不是太复杂。
"反向指针"手法有点棘手,所以在你能够自如运用之前,应该有相应的测试。通常我不花心思去测试访问函数,因为普通访问函数的风险没有高到需要测试的地步,但本重构要求测试访问函数,所以它是极少数需要添加测试的重构手法之一。
本重构运用反向指针实现双向关联。其他技术(例如连接对象)需要其他重构手法。
做法
- 在被引用类中增加一个字段,用以保存反向指针。
- 决定由哪个类————引用端还是被引用端—————控制关联关系
- 在被控端建立一个辅助函数,其命名应该清楚支出他的有限用途.
- 如果既有的修改函数在控制端,让他负责更新反向指针.
- 如果机油的修改函数在被控制端,就在控制端建立一个控制函数,并让机油的修改函数调用这个新建的控制函数
8.8 Change Bidirectional Association To Unidirectional(将双向关联改为单向关联)
两个类之间有双向关联,但其中一个类如今不再需要另一个类的特性.
去除不必要的关联.
动机
双向关联很有用,但你也必须为它付出代价,那就是维护双向连接、确保对象被正确创建和删除而增加的复杂度。而且,由于很多程序员并不习惯使用双向关联,它往往成为错误之源。 大量的双向连接也很容易造成"僵尸对象"∶某个对象本来已经该死亡了,却仍然保留在系统中,因为对它的引用还没有完全清除。
此外,双向关联也迫使两个类之间有了依赖∶对其中任一个类的任何修改,都可能引发另一个类的变化。如果这两个类位于不同的包,这种依赖就是包与包之间的相依。过多的跨包依赖会造就紧耦合系统,使得任何一点小小改动都可能造成许多无法预知的后果。
只有在真正需要双向关联的时候,才应该使用它。如果发现双向关联不再有存在价值,就应该去掉其中不必要的一条关联。
做法
- 找出保存 你想去除的指针 的字段,检查他的每一个用户,判断是否可以去除该指针.
- 如果客户使用了取值函数,先运用Self Encapsulate Field 将待删除字段自我封装起来,然后使用 Substitute Algorithm 对付取值函数,令它不再使用该字段.然后编译,测试.
- 如果客户并未使用取值函数,那就直接修改待删除字段的所有被引用点:改以其他途径获得该字段所保存的对象.每次修改后,编译并测试.
- 如果已经没有任何函数使用待删除字段,移除所有对该字段的更新逻辑,然后移除该字段
- 编译,测试.
8.9 Replace Magic Number With Symbolic Constant (以字面常量取代魔法数)
你有一个字面数值,带有特别含义.
创造一个常量,根据其意义为它命名,并将上述的字面数值替换为这个常量.
动机
在计算科学中,魔法数(magic number)是历史最悠久的不良现象之一。所谓魔法数是指拥有特殊意义,却又不能明确表现出这种意义的数字。如果你需要在不同的地点引用同一个逻辑数,魔法数会让你烦恼不已,因为一旦这些数发生改变,你就必须在程序中找到所有魔法数,并将它们全部修改一遍,这简直就是一场噩梦。就算你不需要修改,要准确指出每个魔法数的用途,也会让你颇费脑筋。
许多语言都允许你声明常量。常量不会造成任何性能开销,却可以大大提高代码的可读性。
进行本项重构之前,你应该先寻找其他替换方案。你应该观察魔法数如何被使用,而后你往往会发现一种更好的使用方式。如果这个魔法数是个类型码,请考虑使用Replace Type Code with Class(218);如果这个魔法数代表一个数组的长度,请在遍历该数组的时候,改用Array.length().
做法
- 声明一个常量,令其值为原本的魔法数值.
- 找出这个魔法数的所有引用点
- 检查是否可以使用这个新声明的常量连替换该魔法数.如果可以,便以此常量替换之.
- 编译
- 所有魔法数都被替换完毕后,编译并测试.此时整个程序应该运转如常,就像没有做任何修改一样.
8.10 Encapsulate Field(封装字段)
你的类中存在一个public字段.
将他声明为private,并提供相应的访问函数.
动机
面向对象的首要原则之一就是封装,或者称为"数据隐藏"。按此原则,你绝不应该将数据声明为public,否则其他对象就有可能访问甚至修改这项数据,而拥有该数据的对象却毫无察觉。于是,数据和行为就被分开了———这可不是件好事。
数据声明为public被看做是一种不好的做法,因为这样会降低程序的模块化程度。数据和使用该数据的行为如果集中在一起,一旦情况发生变化,代码的修改就会比较简单,因为需要修改的代码都集中于同一块地方,而不是星罗棋布地散落在整个程序中。
EncapsulateField(206)是封装过程的第一步。通过这项重构手法,你可以将数据隐藏起来,并提供相应的访问函数。但它毕竟只是第一步。如果一个类除了访问函数外不能提供其他行为,它终究只是一个哑吧类。这样的类并不能享受对象技术带来的好处。而你知道,浪费任何一个对象都是很不好的。实施EncapstlateField((206)之后,我会尝试寻找用到新建访问函数的代码,看看是否可以通过简单的Move Method(142)轻快地将它们移到新对象去。
做法
- 为public字段提供取值/设值函数.
- 找到这个类以外使用该字段的所有地点,如果客户只是读取该字段,就把引用替换为对取值函数的调用,如果修改了,就替换为对设值函数的调用
- 每次修改之后,编译并测试
- 将此字段的所有用户修改完毕后,把字段声明为private
- 编译,测试
8.11 Encapsulate Collection(封装集合)
有个函数返回一个集合.
让这个函数返回该集合的一个只读副本,并在这个类中提供添加/移除集合元素的函数.
动机
我们常常会在一个类中使用集合(collection,可能是array、list、set或vector)来保存一组实例。这样的类通常也会提供针对该集合的取值/设值函数。
但是,集合的处理方式应该和其他种类的数据略有不同。取值函数不该返回集合自身,因为这会让用户得以修改集合内容而集合拥有者却一无所悉。这也会对用户暴露过多对象内部数据结构的信息。如果一个取值函数确实需要返回多个值,它应该避免用户直接操作对象内所保存的集合,并隐藏对象内与用户无关的数据结构。至于如何做到这一点,视你使用的Java版本不同而有所不同。
另外,不应该为这整个集合提供一个设值函数,但应该提供用以为集合添加/移除元素的函数。这样,集合拥有者(对象)就可以控制集合元素的添加和移除。如果你做到以上几点,集合就被很好地封装起来了,这便可以降低集合拥有者和用户之间的耦合度。
做法
- 加入为集合添加/移除元素的函数
- 将保存集合的字段初始化为一个空集合.
- 编译
- 找出集合设置函数的所有调用者.你可以修改那个设置函数,让他使用上述新建立的 添加/移除元素 函数;也可以是直接修改调用端,让他们调用上述新建立的 函数.
- 编译,测试
- 找出所有 通过取值函数获得集合并修改其内容 的函数.逐一修改这些函数,让他们改用 添加/移除 函数.每次修改后,编译并测试
- 修改完上述所有 '通过取值函数获得集合并修改其内容' 的函数后,修改取值函数自身,使他返回该集合的一个只读副本.
- 编译,测试.
- 找出取值函数的所有用户,从中罩住应该存在于集合所属对象内的到吗.运用 Extract Method(110)和Move Method(142)将这些代码移到宿主对象去
- 修改现有取值函数的名字,然后添加一个新取值函数使其返回一个枚举,找出旧取值函数的所有被使用点,将他们都改为使用新取值函数.
- 如果这一步跨度太大,你可以先使用Rename Method(273)修改原取值函数的名称;再建立一个新取值函数用以返回枚举;最后在修改所有调用者,使其调用新取值函数.
- 编译,测试
将行为移到类中
封装数组
8.12 Replace Record with Data Class(以数据类取代记录)
你需要面对传统编程环境中的记录结构
为该记录创建一个 '哑' 数据对象
动机
记录型结构是许多编程环境的共同性质。有一些理由使它们被带进面向对象程序之中∶你可能面对的是一个遗留程序,也可能需要通过一个传统API来与记录结构交流,或是处理从数据库读出的记录。这些时候你就有必要创建一个接口类,用以处理这些外来数据。最简单的做法就是先建立一个看起来类似外部记录的类,以便日后将某些字段和函数搬移到这个类之中。一个不太常见但非常令人注目的情况是∶数组中的每个位置上的元素都有特定含义,这种情况下应该使用ReplaceArray with Object(186)。
做法
- 新建一个类,表示这个记录
- 对记录中的每一项数据,在新建的类中建立对应的private字段,并提供相应的取值/设值函数
8.13 Replace Type Code with Class (以类取代类型码)
类之中有一个数值类型码,但它并不影响类的行为.
以一个新的类替换该数值类型码
动机
在以C为基础的编程语言中,类型码或枚举值很常见。如果带着一个有意义的符号名,类型码的可读性还是不错的。问题在于,符号名终究只是个别名,编译器看见的、进行类型检验的,还是背后那个数值。任何接受类型码作为参数的函数,所期望的实际上是一个数值,无法强制使用符号名。这会大大降低代码的可读性,从而成为bug之源。
如果把那样的数值换成一个类,编译器就可以对这个类进行类型检验。只要为这个类提供工厂函数,你就可以始终保证只有合法的实例才会被创建出来,而且它们都会被传递给正确的宿主对象。
但是,在使用Replace Type Code with Class(218)之前,你应该先考虑类型码的其他替换方式。只有当类型码是纯粹数据时(也就是类型码不会在switch语句中引起行为变化时),你才能以类来取代它。Java只能以整数作为swicch语句的判断依据,不能使用任意类,因此那种情况下不能够以类替换类型码。更重要的是∶任何switch语句都应该运用Replace Conditional with Polymorphism(255)去掉。为了进行那样的重构,你首先必须运用Replace Tpe Codewih Subclsses(223)或Replace Tpe Code with Sate/Srategy(227),把类型码处理掉。
做法
- 为类型码建立一个类
- 修改源类实现,让它使用上述新建的类.
- 编译,测试.
- 对于源类中每一个使用类型码的函数,相应建立一个函数,让新函数使用新建的类.
- 逐一修改源类用户,让他们使用新接口
- 每修改一个用户,编译并测试.
- 删除使用类型码的旧接口,并删除保存旧类型码的静态变量.
- 编译,测试
8.14 Replace Type Code with Subclasses(以子类取代类型码)
你有一个不可变的类型码,他会影响类的行为.
子类取代这个类型码.
做法
- 使用 Self Encapsulate Field(171)将类型码自我封装起来
- 为类型码的每一个数值建立一个相应的子类.在每个子类中腹泻类型码的取值函数,使其返回相应的类型码值
- 没建立一个新的子类,编译并测试.
- 从超类中删掉保存类型码的字段.将类型码访问函数声明为抽象函数.
- 编译,测试
8.15 Replace Type Code with State/Strategy (以State/Strategy取代类型码)
你有一个类型码,他会影响类的行为,但你无法通过继承手法消除它
以状态对象取代类型码.
做法
- 使用 Self Encapsulate Field(171)将类型码自我封装起来
- 新建一个类,根据类型码的用途为它命名.这就是一个状态对象.
- 为这个新类添加子类,每个子类对应一种类型码
- 在超类中建立一个抽象的查询函数,用以返回类型码.在每个子类中腹泻该函数,返回确切的类型码
- 编译
- 在源类中建立一个字段,用以保存新建的状态对象
- 调整源类中负责查询类型码的函数,将查询动作转发给状态对象
- 调整源类中为类型码设值的函数,将一个恰当的状态对象子类赋值给保存状态对象的那个字段
- 编译测试
8.16 Replace Subclass with Fields(以字段取代子类)
你的各个子类的唯一差别只在'返回常量数据'的函数上.
修改这些函数,使他们返回超类中的某个字段,然后销毁子类
做法
- 对所有子类使用 Replace Constructor with Factory Method(304)
- 如果有任何代码直接引用子类,令它改变引用超类.
- 针对每个常量函数,在超类中声明一个final字段
- 为超类声明一个protected 构造函数,用以初始化这些新增字段
- 新建或修改子类构造函数,使它调用超类的新增构造函数
- 编译,测试
- 在超类中实现所有常量函数,令他们返回相应字段值,然后将该函数从子类中删掉.
- 没删除一个常量函数,编译并测试
- 子类中所有的常量函数都被删除后,使用Inline Method(117)将子类构造函数内联到超类的工厂函数中
- 编译,测试
- 将子类删掉
- 编译,测试
- 重复'内联构造函数,删除子类'过长,知道所有子类都被删除
- 本文链接: https://halo.cjh.kim/archives/重构改善既有代码的设计8
- 版权声明: 本博客所有文章除特别声明外,均采用CC BY-NC-SA 3.0 许可协议。转载请注明出处!