《架构整洁之道》&《 重构改善既有代码的设计》 整合

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/qq_32250495/article/details/86483693

一、重构定义:

在不改变软件可观察行为的前提下,对软件内部进行调整(使用重构手法),以提高其可理解性,降低其修改成本。 -- 重点在两点: 1. 不改变软件的可观察行为。2. 提高其可理解性。

两个目的:1. 添加新功能。 2. 重构。 --重构就只管修改程序结构,不要添加新功能。 添加新功能就不要修改既有代码。两者混合进行会使得程序朝不可理解的方向发展。

ps: 重构与设计模式具有辩证的关联性,模式是目的,重构是到达之路。重构促进设计模式的形成与稳定,模式为重构提供前进方向,二者相辅相成,具有统一性。


二、为什么需要重构

1. 面对迅速变化的需求,对原有的代码进行修改十分困难(逻辑复杂,条理不清晰,很难兼顾;改动接口多,测试困难), 尤其对于某些无法限定影响面的接口修改。
2. 使得原有设计保持本真意义。代码结构的流失具有累积性,原有的设计及意图难以保持,阅读源代码很难理解原来的设计。
3. 消除重复代码,重复代码越多,修改的风险越大,修改带来的不一致性可能越大(修改一处,未修改另一处)
4. 使得软件更容易理解,结构更清晰,代码更简洁。
5. 帮助找到bug
6. 提高编程速度 -- 维持良好设计,清晰的意图,从而使得编程更加容易。
7. 重构与新功能 -- 如果你发现自己需要添加一个特性,而代码结构使你无法很方便地达成目的,那就先重构那个程序,使特性比较容易进行,然后再添加特性。
8. 重构与性能 -- 重构调整代码结构,使性能调优更加方便的进行。

2.1    架构整洁之道 ——Robert C. Martin

2.1.1. 综述

为什么要进行架构设计: 采用好的架构可以大大节省软件项目构建与维护的人力成本。每次变更都短小简单,易于实施,并且避免缺陷,用最小的成本,最大程度的满足功能性的需求。在软件的全生命周期内,最大化程序员的生产力,同时最小化系统的总运营成本。

怎样才是良好的架构设计: 良好的架构设计让系统便于理解,易于修改,方便维护,轻松部署,不依赖于成堆的脚本与配置文件,也不需要用户创建有严格要求的目录与文件。

怎样才是优秀的架构设计师:架构师的职责在场景需求中,整理出核心的业务流程。对业务进行边界划分,同时管理好各个边界的依赖。优秀的架构师往往对业务场景有较好的抽象能力,能够cover住场景类各业务实体变化的需求。 同时优秀的架构师能够预留出不完全边界,在以后业务拆分重构中以微小易实施的改动进行。最后优秀的架构师为系统预留了许多的选项(方便的添加),将技术手段的决策延后(使用哪种框架等决策),并做到设备无关,使业务的发展不依赖于具体的框架、语言、数据库等技术细节。

2.1.2 两个维度的价值:

1. 行为价值: 按照需求文档写代码,修复Bug
2. 架构价值: 软件必须可以以一种灵活的方式来改变机器的工作行为 (ps: 这种灵活体现在对需求的扩展性上, 而场景抽象能力是一位架构师的基础能力)

总结: 一次性消费的代码,在需求变更过程中价值趋于0, 而良好设计的架构能够满足不断变化的需求(场景内), 从而带来持续的价值。
一个软件架构师,更应该关注系统的整体架构,而不是具体的功能和系统行为的实现。软件架构师必须创建出一个可以让功能实现起来容易、修改起来更简单、扩展起来更轻松的软件架构。
如果系统越来越难维护,终导致无法修改,说明软件工程师没有完成自己应尽的责任,是软件开发的失败。

2.1.3. 编程范式

2.1.3.1  结构化编程: 对程序控制器直接转移的限制

将一段程序递归降解为一系列可证明的小函数,然后通过编写相关的测试来证明这些函数都是错误的。如果无法证伪这些函数,那么久可以认为这些函数是足够正确的。
可以说结构化编程,功能性降解,然后证伪仍然是软件架构设计领域的最佳实践。

2.1.3.2 面向对象编程: 对程序控制器间接转移的限制

依赖反转:让对源代码实现的依赖变成对接口的依赖,而源代码实现依赖接口,让源代码实现可以随意替换。
多态可以对源代码的中的依赖关系进行控制,从而让代码成为一种插件式的架构,对于组件的替换基本不需要进行代码改动。经过高层策略组件与底层实现的分离,从而做到真正的独立部署、独立开发。
同时面向对象的多态通过隐式的方式来使用函数指针,从而保证了函数指针的安全性。

2.1.3.3 函数式编程: 对赋值操作的限制

如果计算能力很大,存储能力足够。对于任何可变变量,都可以通过函数计算来获取。这样程序始终处于无状态,无锁竞争之中,处于不变之中。这就是函数式编程。

2.1.4  设计原则

        (其最终目的都是为了建立一个松耦合的结构,让系统可以灵活的扩展,以满足场景内需求的变化)

2.1.4.1 solid原则

在构建中层模块结构时候,将数据和函数进行合适的分类组合。主要目标: 使软件可容忍被改动;使软件容易被理解;构建可在多个软件系统中复用的组件


1. 单一职责原则: 每个软件模块都有且只有一个需要被改变的理由->任何一个模块都应该只对一个用户,或系统利益相关者负责->任何一个模块都应该只对某一个行为者负责。
软件模块:一组紧密相关的函数和数据结构, 更笼统的说 就是指一个类。 用户:更抽象的说是一个需求。
单一职责让一个模块或一个函数,只对一个用户或者高层负责。 避免不同用户对同一模块的依赖从而造成混乱。

2. OCP开闭原则: 设计可以通过新增加代码来修改系统行为,而不是靠修改原来的代码。对修改关闭,对新增开放。
这需要保存高层抽象的稳定性,让底层实现依赖于高层代码,只需要增加底层的不同实现即可。

3. LSP里式替换原则:如果想用可替换的组件来构建软件系统,那么这些组件必须遵守同一个约定。任何使用父类的地方,都可以用其子类替换。

4. 接口隔离原则:在设计过程中避免不必要的依赖,组件不应该依赖,其不必要的东西(所依赖的类包含无关的东西)
类似迪米特原则:在类的层级依赖关系中,当前类只与上下两层的类发生依赖,不隔层依赖,不同层依赖。这样避免了不必要的依赖,从而减少修改的扩散风险,同时为分离部署和单独编译带来可可能。

5. DIP依赖反转: 高层策略性代码不应该依赖底层细节的代码。底层实现细节代码应该依赖高层策略性代码。

ps: 单一职责,与接口隔离的统一点: 单一职责使得某一个类或组件只服务于某一功能,另一个类或组件服务于另一功能,这自然做到了接口隔离,从而可独立发展,部署。
但单一职责体现变更层面的意思:某一个类只由于一个原因而变更,不因为不同原因而变更。
而接口隔离体现依赖层面的意思:某一个类不依赖其他不相干的类。既然类都是单一职责了,自然不会产生依赖,即使有依赖,可以通过导演类,全局变量来解决。


2.1.4.2 组件聚合原则

REP原则(复用/发布): 软件复用的最小粒度,等同于其发布的最小粒度。 REP原则指组件中的类与模块彼此紧密相关,他们必须有一个共同的主题或者大方向,从而成为一个可独立发布的可复用模块
CCR原则(共同闭包):将那些会同时修改,并且为相同目的而修改的类放到同一个组件中,与单一职责类似,只不过其服务的层次更高,更抽象。
CRP原则(共同复用): 比较常见的情况是多个类同时作为某个可复用的抽象定义,被共同复用。应该将这些类放在同一个组件中。

2.1.4.3 组件耦合原则

无环依赖原则: 如果组件间的依赖形成一个环,则这些组件必须共同修改,共同发布,共同打tag,同时无法进行分解测试,必须集成测试。相反如果单向依赖,则各组件可以单独发布版本,依赖他的模块只需控制版本即可。依赖反转可以解决循环依赖。
稳定性依赖原则: 从依赖的上游,到依赖的下游,稳定性一次递增。对于需要具有高稳定性的下游模块,其变更的工作量,远远大于新增工作量。抽象的高阶策略代码,往往具有场景稳定性,所以需要根据组件的稳定性程度来觉得其抽象程度。换句话说:稳定性依赖指明依赖应该指向更加抽象的方向。

不稳定性计算公式: 出依赖/(出依赖+入依赖) 抽象化程度: 组件中接口与抽象类的数量/类的总数量

可以指出,一个组件的抽象化程度与被依赖数据是相关的,一个组件如果被依赖数量较大,则其应该被设计的更加抽象;相反其依赖数量较少,则其需要更容易变动。
一个完全抽象的组件是毫无用处的,只有可变更,可扩展的组件才能支撑业务的不断发展。一个优秀的组件应该在其所处的业务层次对两者进行权衡。《架构整洁之道》设计了一套方法来衡量一个组件的依赖设计

2.1.5 整洁架构设计

        整洁架构的设计,应该遵从上述职责单一,依赖反转等主要原则。以业务为中心,让其他技术手段直接或间接依赖于业务。在系统开发过程中,要时刻关注模块间的依赖关系,在必要时进行边界拆分重构。

                       图2.1:整洁架构设计

2.1.5.1 用例驱动

        以业务实体为中心设计用例,对用例进行分类从而划分出场景下的不同业务流程。同时根据用例可以方便的进行设计测试案例,

2.1.5.2 边界划分

        架构设计最核心的部分就是边界划分。对于业务流程,如何划分边界使得一部分业务独立发展和部署是架构设计最关注的地方。

        单体架构降低了团队间项目研发的效率,所以往往将业务分模块独立开发和部署。这便要求需要有很好的业务边界,使得整块业务独立,跨边界依赖清晰并且服务依赖稳定性原则。

        在同一个业务模块内,对于部分更小的、具有独立性的业务,应该进行预留边界划分,以便于以后重构需要业务拆分的时候,方便的独立出去。

2.1.5.3 场景抽象

        在业务场景中,包括整个系统的和模块内部的,应该意识到业务实体的可变性。需要在各自的场景类进行抽象,以保持业务策略的稳定性,包容业务实体的易变性。如:打车场景下打车的用户可以是人,也可以是动物或者货物;开车的司机不一定是人,也可能是机器人(自动驾驶)等。

2.1.5.4 接口设计

        业务边界的划分本质上就是设计良好的开放接口,以便跨边界调用。同时在内部的不完全边界设计良好的开放API。 在技术实现上通过设计良好的接口,让多种技术手段依赖于接口做到依赖反转,从而形成一种插件式的架构。例如数据库的实现:首先为业务设计好需要的数据库操作接口,而不管使用哪种数据库,怎么样实现。不同的数据库通过实现这个接口定义,从而将自己做成一种插件,可以随时替换进来。

2.1.6. 常用架构设计

2.1.6.1  分层架构

        整个系统按照层次进行划分如:展示层,控制层,模型层,持久层。每一层囊括多种业务。但是业务混杂在一起,很难进行隔离 如 进程隔离,线程池隔离等。

2.1.6.2  功能架构

        整个系统按照功能进行划分,一个功能可能包含链路的多个层面。但是划分太细、依赖太复杂从而难于管理。

2.1.6.2 SOA架构

        SOA架构是按照组件进行划分,一个模块内可囊括多种功能,包含多个层次。但是这些业务组合在一起形成一个单一的服务,支撑一类业务。 比较常见的SOA架构是按照业务链路进行分层解耦,每一个业务层是一个独立的服务。

2.1.6.3 微服务架构

        微服务架构与SOA架构同类,但是微服务的边界划分更细。微服务不是只要划分更加细的服务,微服务的核心在业务的横向拆分,将多个垂直应用的共同业务划分成一个独立的部分,从而提高组件复用率,方便进行统一的管理,独立的开发和部署。这个跨多个应用的服务,通常表现的比SOA服务更加微小。

2.1.6.4 小结架构设计方法步骤

        1. 根据上述整洁架构原则,首先要分析整个系统要支撑的业务场景,画出场景下的用例图。根据用例图分析,该用例下需要进行的数据转换流程。数据流图可以清楚的描绘数据的输入、输出已经转换过程。

        2. 根据用例图和数据流图,已经能清楚的描绘一个场景下,系统应该支撑的业务流程。此时就行业务流程建模,描绘该系统从输入数据到输出的所有业务流程。(注意在这个阶段不要考虑用何种技术手段去实现这个过程,比如: web还是pc,何种数据库,输出哪些字段等问题)。

        3. 将多个业务流程划分边界。不管如何划分边界,主要考虑提高团队的协作开发效率,资源利用率。同时为未来可独立开发部署的业务预留出不完全边界。

        4. 进行边界划分后,可以进一步用时序图来表达场景下,某类用例业务各边界的交互依赖。用活动图表达边界内业务处理的具体动作流程。

        5. 明确边界内的业务动作后,可以对业务进行抽象设计,从而保持业务策略代码的稳定性,支撑业务实体的变化性。

        6. 明确边界间的依赖关系后,可以对业务边界设计详细的接口。 在不完全边界上也要设计接口或者抽象的API来调用。

三、坏代码的味道(什么样的形式才是坏代码)

重复代码
过长函数
过大的类
发散式变化: -- 一个类收到多个因素的影响,将朝着多个不同的方向变化,可以将类拆解成多个不同的类,然后桥接起来。
散弹式变化: -- 一个类的变动会印象多个其他的类,则可以将受此类影响的代码都放入这个类中。
同步式变化: -- 两个东西总是一起变化,如引用与数据,则将这两个同时变化的东西放入同一个类中。
数据泥团: -- 一些零散的数据项出现在许多不同的类,或者函数的参数列表中。则可以将这些相同数据项抽取到一个类中。
基本类型偏执 -- 对于可以运用对象来表述一个物体,不用基本类型。如日期,时间等
多态扩展: -- 替换switch case 根据类型多态扩展。
平行继承体系 -- 如果为一个类增加子类,必须为另一个类增加子类。可让一个体系引用另一个体系实例
冗赘类 -- 一个类没有多大的实际意义,没必要让人花时间、精力来了解
夸夸奇谈的未来性 -- 某些代码没有多大作用,只是觉得未来可能有用,从而使得系统更难维护和理解
令人迷惑的暂时字段 -- 某些字段只在特殊情况下才被使用到,让人不知其设置目的
过长的调用链路 -- 客户端需要很长的调用链来获取一个值,可以通过委托来获取最终值
中间人 -- 存在过多不必要的委托
狎昵关系 -- 类之间的调用关系较多,依赖较强。可以将一些方法或字段,移动到需要的类中。
异曲同工的类 -- 两个函数做同一件事情,却有着不同的签名。可以根据用途来命名,并抽离重复代码。
不完美的类库 -- 类库缺少需要的功能
纯稚的数据类 -- 未对数据进行封装,数据接口暴露
被拒绝的遗赠 -- 子类完全未使用父类的代码
过多的注释 -- 让代码自身具有注释功能

四、何时重构:

1. 三次法则 --代码忍受了3次,就不要再忍受了,重构吧!
2. 添加新特性时候重构 -- 新特性的添加需要修改原先代码,且修改方案复杂
3. 复审代码时重构 -- 代码评审 谈谈如何重构
4. 重复代码过多
5. 函数过长 -- 一个函数应该只包含一个独立的逻辑功能。 这样将逻辑单元打散,可以减少代码耦合,并且更加清晰。
6. 过大的类 -- 一个类中出现太多实例变量,容易出现重复混乱的代码,可以考虑重构出子类。
7. 参数列表过长

五、重构难题

1. 数据库改动 -- 可以增加数据库接入层,使得数据库的变化和业务的变化分离
2. 接口改动 -- 对于已经发布的接口,并被不可控系统使用的时候,则不能重构接口
3. 设计的改动 -- 对系统已有的设计进行重构,不如重新设计并实现。
4. 项目十分紧张 -- 对于业务十分紧张的项目,不应该重构。

六、如何重构

6.1 重构的一些原则

1. 可靠的测试环境。 --构建自动化单元测试套件(首推TestNG,尽量做到逻辑、边界全覆盖, 预期抛出的异常也要测试),并有独立的团队进行功能(黑盒)测试,测试出现bug再通过单元测试定位bug。ps:在写单测的过程中可以适当关注下代码运行性能。

2. 以微小的步伐修改程序。犯错可以很容易发现,配合可靠的测试。 重构节奏: 小修改,测试;小修改,测试;小修改,测试........ -- 重构十分强调微小的修改步伐, 便于测试和审查,以减少引发bug的风险
3. 在重构函数内部修改变量名称,清晰贴切。
4. 函数应该与它所使用的数据结构在同一对象内。
5. 查找引用 -- 通过文本工具查找你需要修改地方的引用,同时利用编译器发现错误。对于被反射引用的地方,一定要记录并进行详细的测试。

ps:如何面对不断变化的世界,如何面对不断变化的需求。 只有深层次的抽象出不变的对象,然后将变化东西放在叶子类中,与其维护的数据结构同在, 通过继承与多态来应对这种变化。

6.2 重构手法

6.2.1、重新组织函数

6.2.1.1  提炼函数 Extract Method

* 概述:将一段代码放入一个独立函数中,并让函数名称解释该函数的用途。
* 动机:厌恶过长的代码。如果函数过长,则很难被理解(用途不清晰,需要大段的注释描述意图),测试也困难;相反,如果一个函数够小、有清晰意图的命名,则被复用的机会更大,函数也更容易被修改。
* 做法:编写一个新函数,根据这个函数的意图来命名 -- 如果能对一段代码给予一个更好表达其意图的命名,都可以提炼她。
* 注意:这个重构手法最困难的地方在于提炼局部变量:
1. 如果需要读取源函数的局部变量,则可将其作为参数传入新函数。
2. 如果某些局部变量需要在目标函数后面使用,可能需要新函数返回该局部变量改变的值。
3. 对于需要修改多个局部变量的情况,则可不提炼此段代码,或者将所有临时变量抽取为一个类。

6.2.1.2  内联函数 Inline Method

* 概述:在函数调用点插入函数本体,然后移除该函数
* 动机:1.有些函数的内部代码比函数名本身更加清晰易读,则可以内联。2.函数调用混乱,可以先全部内联在一起,然后重新提炼。
* 做法:检查多态覆盖,检查函数引用,引用点直接替换为函数。

6.2.1.3. 内联临时变量 Inline Temp

* 概述:将所有对该变量的引用动作,替换为对她赋值表达式。
* 动机:临时变量可能妨碍其他的内联手法。
* 做法:确保只被一个简单的表达式赋值了一次

6.2.1.4. 以查询取代临时变量 Replace Temp with Query

* 概述:程序以某个临时变量保存表达式的运算结果。则将一个表达式提炼到一个函数中,将将对这个临时变量的引用替换为对新函数的调用
* 动机:临时变量总是驱使你写更长的函数,因为临时变量只在函数内可以访问到,所以将一些运算得到的临时变量提炼为一个函数,可以在任何时候访问。ps: 导致重复计算,但是使代码清晰。
* 做法:确保该临时变量只被赋值一次,然后用新函数替换引用处。可以先将该临时变量声明为final,让编译器保证只赋值一次。

ps: 1,2,3,4的操作都让代码结构变得更加清晰、简洁,使得函数名称本身便具有自注释功能。

6.2.1.5. 引入解释性变量 Introduce Explaining Variable

* 概述:将复杂表达式(或其中一部分)的结果放入一个临时变量,以这个变量的名称来解释表达式用于
* 动机:将一个复杂的表达式进行拆解,用临时变量的名称来表达部分过程的意图。

6.2.1.6. 分解临时变量 Split Temporary Variable

* 概述:程序中某个临时变量有多个用途,既不是循环变量,也不是收集计算结果变量,则应该对每个用途编写一个临时变量
* 动机:临时变量有不同的用途,或者由不同含义的操作计算而得到,则不应该使用同一临时变量。

ps: 5,6的拆解都是为了使得表达式的意图更加清晰,各种复用临时变量,复杂计算过程等代码都可能让代码变得含混。

6.2.1.7. 移除对参数的赋值 Remove Assignments to Parameters

* 概述:代码对一个参数进行赋值,应该用一个临时变量取代参数的位置。
* 动机:对参数的赋值可能改变其引用的对象(引用参数), 从而丢失了原来的入参引用。所以将参数赋值给一个临时变量以改变这种含混性。

6.2.1.8. 以函数对象取代函数 Replace Method with Method Object

* 概述: 有一个大型函数,其中过多的局部变量无法进行函数提炼,则将这个放入一个单独对象中,这样局部变量就成了对象的成员变量,然后在对象内将大型函数分解为多个小函数
* 动机: 局部变量太复杂,根本无法拆解, 则使用一个对象来描述该方法。 -- 该对象需要有一个与方法意图对应的命名。
* 做法: 1. 建立一个新类,并将原类作为该类的一个常量引用
2. 新类中提供构造函数接收原对象,及原函数的所有参数。
3. 在新类中提供一个函数,将原函数的代码复制到其中,通过原对象引用相应变量。

6.2.1.9. 替换算法 Substitute Algorithm

* 概述:将函数本体替换为另一个算法
* 动机:原函数的算法实现比较复杂,切不清晰,或者性能不好。如果你发现有更好的实现方式,勇敢的壮士断腕,替换该算法吧。


6.2.2、在对象间搬移特性


6.2.2.1.  搬移函数 Move Method

* 概述:将一个函数移动到另一个类中去
* 动机:类中一个函数与其他的类进行频繁的交流,而与本类交流较少,则将函数移到另一个类中,从而减少调用关系。
* 做法:搬移一个或者一组相关的函数;检查源类的子类和超类是否有该函数的其他声明(覆盖,委托等问题);修改源函数使之成为一个委托函数。

6.2.2.2. 搬移字段 Move Field

* 概述:将类中一个字段搬移到其他类中
* 动机:本类中的一个字段,被其他类中的字段使用次数更多,搬移字段减少调用关系,间接提高信息隐藏性。
* 做法:如果字段被很多地方引用,则先使用自我封装(Self-Encapsulation)将字段变成设/取值函数,然后搬移字段,最后修改设/取值函数为一个委托。

6.2.2.3. 提炼类 Extract Class

* 概述:建立一个新的类,将相关的字段和函数从旧类中搬移到新类
* 动机:1. 一个类的方法过多、责任过大做了由两个类做的事情。主要是由于当前类对于场景的抽象不够细致,导致过于复杂。
2. 一个类中的数个特性朝着不同方向发展,从而变得无所关联。最主要的表现是子类化,某些特性需以一种方式子类化,某些特性以另一种方式子类化 
* 做法:从一个类中搬移函数和字段到一个新类中,使得新类作为一个事物的更细致抽象。
* 例如:一个person类有地址字段和函数,比较复杂。可以将地址抽象为一个新类,然后person调用这个地址类。

6.2.2.4. 内联类 Inline Class

* 概述: 将一个类的所有特性搬移到另一个类中,然后移除该类
* 动机: 这个类没有做太多事情
* 做法: 上述的反过程

6.2.2.5. 隐藏委托关系 Hide Delegate

* 概述: 在服务类建立客户所需的所有函数,用以隐藏委托关系
* 动机: 对内部进行封装,通过一个委托类来调用其他对象,这样调用者可以不用了解内部变化,从而降低修改风险。
* 做法: 正如设计模式中的代理模式等, 不仅隐藏调用关系,还可以对调用进行统计等管理。

6.2.2.6. 移除中间人 Remove Middle Man

* 概述: 直接调用受托类
* 动机: 某个类做了过多的简单委托,每添加一个新特性都需要在委托类中进行相应添加。
* 做法: 上述反过程

6.2.2.7. 引入外加函数 Introduce Foreign Method

* 概述: 在客户类中建立一个函数,并以第一参数形式传入一个服务类实例
* 动机: 由于代码修改权不在自己手里,但是需要为某个类添加新特性。可以在外层写这样一个函数,并所需要的值都以参数传入。
* 做法: 在拥有代码所有权后,将此函数返回她该去的地方

6.2.2.8. 引入本地扩展 Introduce Local Extension

* 概述: 当需要为一个服务类提供一些额外的函数,但是你无法修改。可建立一个新类,使她包含这些额外的函数。让这个扩展类成为源类的子类或者包装类
* 动机: 为类做扩展,可以使用装饰模式或者继承,使用继承请确保必定有继承关系。

ps: 从以上重构方法可以看出,其最终都是为了减少了代码中的调用关系,提高代码的意图表达。

6.2.3、 重新组织数据

6.2.3.1. 自封装字段 &封装字段 &封装集合 Self Encapsulate Field & Encapsulate Field & Encapsulate Collection

* 概述:将字段进行封装,然后提供设/取值函数,对于集合取值只能返回可读的副本
* 动机:隐藏内部数据接口,这样就算字段数据结构发生变化,只需要调整设/取值函数,从而不影响其他类的调用; 对于集合需要防止外部对引用的修改; 方便上syncrized关键字

6.2.3.2. 以对象取代数据值 Replace Data Value with Object

* 概述:将数据项变为字段
* 动机:数据项为对一个事物属性的描述,你无法估量这个场景下该事物属性的复杂性,所以用一个类构建该数据项,能够抗击未来的变化。

6.2.3.3. 将值对象改为引用对象 Change Value to Reference

* 概述:将这个值对象变成引用对象
* 动机:对一个对象修改的数据,能够影响引用这个对象的地方

6.2.3.4. 将对象引用改为值对象 Change Reference to Value

* 概述:将引用对象变为一个值对象
* 动机:利用其不可变性质

6.2.3.5. 用对象取代数组 Replace Array with Object

* 概述:以对象替换数组,对于数组中的每个元素,以一个字段来表示
* 动机:有些其他语言,将一个对象的属性存放在一个数组中了。将其重构为对象,从而可用字段名逐一描述

6.2.3.6. 复制被监视数据 Duplicate Observed Data

* 概述:将数据复制到一个领域对象中,建立一个observer模式,用于同步领域对象和GUI对象内的数据
* 动机:分离界面显示和业务逻辑,在界面更新数据的时候同步更新领域对象数据

6.2.3.7. 将单向关联改为双向关联 Change Unidirectional Association to Bidirectional

* 概述:添加一个反向指针,并使修改函数能够同时更新两条连接
* 动机:使得两个类相互依赖,方便访问对方的成员。 虽然方便,但是调用关系复杂,容易产生僵尸对象,修改时相互影响。

6.2.3.8. 将双向关联改为单项关联 Change Bidirectional Association to Unidirectional

* 概述:去掉不必要的关联
* 动机:降低类间复杂度

6.2.3.9. 以字面常量取代魔法数 Replace Magic Number with Symbolic Constant

* 概述:创造一个常量,根据其意义命名,并将有特殊意义的字面数值替换为这个常量
* 动机:魔数-有特殊意义的数字,使用一个常量来表示这个数字的意义

6.2.3.10. 以数据类取代记录 Replace Record with Data Class

* 概述:面对传统编程的记录结构,为该记录创建一个'哑'数据对象
* 动机:与数据库的记录进行交互,ORM

6.2.3.11. 以类取代类型码 Replace Type Code with Class

* 概述:用一个新的类替换该类中用常量数值表示的类型
* 动机:其实就是使用枚举类型来代替常量枚举,这样使得类型被更有意义的描述,并且提供了对应的操作函数,对于复杂的类型十分方便。

6.2.3.12. 以子类取代类型码 Replace Type Code with Subclasses

* 概述:为类型码建立一个继承宿主类的子类,将根据这个类型码执行的操作,放入到一个子类中去。
* 动机:将不同类型码的操作都放入对应的子类中,这样不经让代码更清晰,同时防止错乱。

6.2.3.13. 用状态或者策略取代类型码 Replace Type Code with State/Strategy

* 概述: 类中有类型码会影响类的行为,但是无法通过继承手法来消除
* 动机: 无法直接继承宿主类,则自己定义一个抽象的类去继承,与上述一致。

6.2.3.14. 以字段取代子类 Replace Subclass with Fields

* 概述: 各个子类的差别不是打,只在一些常量数据上
* 动机: 将子类中的共同行为搬移到超类,并在超类中定义这组差异的常量。减少无必要的编码

6.2.4、 简化条件表达式

6.2.4.1. 分解条件表达式 Decompose Conditional

* 概述:有一个复杂的条件表达式,从if then else中分别提炼出独立函数
* 动机:复杂的条件逻辑常常导致代码复杂度的提高,提炼不同分支,并按照意图命名,是代码可读性和清晰提高

6.2.4.2. 合并条件表达式 Consolidate Conditional Expression

* 概述:有一些列的条件测试,但是都返回相同结果,则合并这些表达式
* 动机:有一些表达式虽然条件不同,但是结果一样,将这些条件合并

6.2.4.3. 合并重复的条件片段 Consolidate Duplicate Conditional Fragment

* 概述:条件表达式的每个分支有相同的一段代码,将这些代码抽取出来,放入表达式之外
* 动机:将每个分支都执行的代码,搬移到代码执行分支之外,简化分支表达式,以使代码更加清晰。

6.2.4.4. 以卫语句取代嵌套条件表达式 Replace Nested Conditional with Guard Clauses

* 概述:用卫语句处理特殊情况,卫语句就是if then return这种形式的表达式,常用于函数入口处,保护函数体只接受正确的参数。
* 动机:对于情况比较特使的逻辑,使用此表达式可以简化代码,使之更加清晰

6.2.4.5. 以多态取代条件表达式 Replace Conditional with Polymorphism

* 概述:将条件表达式的分支,放入每个子类的覆写函数中,并将原始函数声明为abstract
* 动机:子类覆盖超类的条件表达式,在子类中只关注与自己相关的条件和行为。这样将集中在一起的逻辑打散到各个子类中。

6.2.4.6. 引入Null对象 Introduce Null Object

* 概述:当需要再三检查对象是否为null,则将null值替换为一个null对象
* 动机:为原类建立一个子类,其行为就是原类的null版本,可以方便多态的进行。空对象的存在,可以省去判空逻辑(判空逻辑只写在Null对象内部,其他写在超类)。

6.2.4.7. 引入断言 Introduece Assertion

* 概述:某段代码需要对程序状态做出某种假设,以断言明确表现这种假设
* 动机:断言可以帮助阅读程序代码所做的假设,可以在调试和调试,测试中广泛使用

ps: 这些方法都为了减少条件表达式的复杂性,提高其清晰度和意图,使用多态和空对象的方式,在一定程度上为编程提供方便。

6.2.5、 简化函数调用

6.2.5.1. 函数改名 Rename Method

* 概述:修改函数名称
* 动机:函数的名称未能表示函数的意图

6.2.5.2. 增加参数 Add Parameter

* 概述:为函数添加一个对象参数,让这个对象带进函数所需的信息
* 动机:某个函数需要从调用端获取更多的信息

6.2.5.3. 移除参数 Remove Parameter

* 概述:将函数参数移除
* 动机:函数本体不在需要某个参数,而参数列表却有

6.2.5.4. 将查询函数和修改函数分离 Separate Query from modifier

* 概述:建立两个不同的函数,其中一个负责修改,另一个负责查询
* 动机:获取值得时候会修改函数值,会使得其他只需要取状态值得调用者关心更多的东西,从而产生副作用

6.2.5.5. 令函数携带参数 Parameterize Method

* 概述:建立单一函数,以参数表达那些不同的值。
* 动机:多个函数做着类似的工作,只是因为少数几参数,或者参数个数不同。则可以使用一个单一函数将他们统一起来,为这个函数增加携带参数

6.2.5.6. 以明确函数取代参数 Replace Parameter with Explicit Methods

* 概述:针对该参数的每一个可能值,建立一个独立函数
* 动机:有一个函数,取决于不同的参数值而采取不同的行为,这时可以为每个独立之建立一个独立函数。 某种程度与上一条相反

6.2.5.7. 保持对象完整 Preserve Whole Object

* 概述:改为传递整个对象
* 动机:从某个对象中取出若干值,并将他们作为某一次函数调用参数的时候,可以将参数修改为传递整个对象,以减少参数列表和对抗未来改动的风险。

6.2.5.8. 以函数取代参数 Replace Parameter with Methods

* 概述:让参数接受者去除该项参数,并直接调用前一个函数
* 动机:如果函数可以通过其他途径取得参数值,那么久不应该通过参数值获取。这样可以减少参数列表,从而减少局部变量。

6.2.5.9. 引入参数对象 Introduce Parameter Object

* 概述:以一个对象取代这些参数
* 动机:某些参数总是很自然的同时出现,当一组参数总是被同时传递给多个函数时候,则将这些参数整合成一个对象

6.2.5.10. 移除设置函数 Remove Setting Method

* 概述:去掉字段中的所有设置函数
* 动机:对象中的某个字段只应该在对象创建的时候被设置,然后就不再改变,则不因该提供设值函数,否则可能导致其值被修改,且容易混淆

6.2.5.11. 以工厂函数取代构造函数 Replace Constructor with Factory Method

* 概述:将构造函数替换为工厂函数
* 动机:通过静态工厂函数来生产对象,从而将对象生产权利把控在本类中,调用者完成不需要关系这个新对象的构建过程

6.2.5.12. 封装向下转形 Encapsulate Downcast

* 概述:将向下转形动作移到函数中
* 动机:某个函数返回的对象需要调用者自己强转类型。这是可以将强转类型封装在函数内部,使得调用者无需关心实际类型。需要该函数返回类型有限且确定。

6.2.5.13. 以异常取代错误码 Replace Error Code with Exception

* 概述:将错误码改用异常
* 动机:抛出异常能够更加清楚知道代码执行过程产生的问题。

6.2.5.14. 以测试取代异常 Replace Exception with Test

* 概述:修改调用者,使它在调用函数之前先做检查
* 动机:异常只应该用于哪些产生意料之外的错误行为,而不应该成为条件检查的替代品。可以提供一个可重复执行的测试函数,让调用者者调用前先检查某个条件,在测试函数中处理try catch

ps: 从上述方法可以看出,其最终都是为了减少函数间传参的复杂度,从而减少了局部变量个数,也就提高的代码清晰度。

6.2.6、处理概括关系

6.2.6.1. 字段上移 Pull Up Field

* 概述:将字段移到超类
* 动机:两个子类拥有相同的字段

6.2.6.2. 函数上移 Pull Up Method

* 概述:将函数移动至超类
* 动机:有些函数各个子类中产生完全相同的结果

6.2.6.3. 构造函数本体上移 Pull Up Constructor Body

* 概述:在超类中新建一个构造函数,并在子类中调用她
* 动机:各个子类中拥有本体几乎一致的构造函数

6.2.6.4. 字段下移 Push Down Field

* 概述:将这个字段移动到需要她的类中
* 动机:超类中某个字段只被部分子类用到,让数据与操作在同一个类中。

6.2.6.5. 提炼子类 Extract Subclass

* 概述:新建一个子类,将只被某些实例用到特性移动该子类中
* 动机:子类划分不够具体,不够细致,导致某个类包含过多本不该自己管理的东西,这是可以提炼一个新的子类。

6.2.6.6. 提炼超类 Extract Superclass

* 概述:为两个类建立一个超类,将相同的特性移至超类
* 动机:两个类具有相似的特性

6.2.6.7. 提炼接口 Extract Interface

* 概述:将相同的子集提炼到一个独立接口中
* 动机:1. 若干客户端使用类接口中的同一子集;2. 两个类的接口有部分相同。这两种情况都可以抽出一套共有接口。

6.2.6.8. 折叠继承体系 Collapse Hierachy

* 概述:将继承体系合为一体,以消除继承体系
* 动机:超类和子类,并没有太大的区别。 ps:

6.2.6.9. 塑造模板函数 Form Template Method

* 概述:模板模式,不再概述

6.2.6.10. 以委托取代继承 Replace Inheritance with Delegation

* 概述:装饰模式,不再概述
* 动机:没有继承关系的两个类使用继承,不仅没有代码复用,还继承了父类大堆不相干方法,容易造成混淆;对于抽象的方法还必须覆写。而装饰模式可以自主选择需要复用的方法。ps:非继承关系的扩展请使用装饰模式;有继承关系的扩展请使用继承

6.2.6.11. 以继承取代委托 Replace Delegation with Inheritance

* 概述:对于有继承体系的仍然使用继承体系,这样可以复用父类的方法和字段。

6.2.7、大型重构

6.2.7.1. 梳理并分解继承体系 Tease Apart Inheritance

* 概述:建立两个继承体系,并通过委托关系让其中一个可以调用另一个
* 动机:某个继承体系同时承担两项责任,将此继承体系拆解,使得抽象的分类更清楚、细致从而提高类的复用率,并使代码更简洁。

6.2.7.2. 将过程设计转化为对象设计 Convert Procedural Design to Objects

* 概述:将数据记录变成对象,将大块的行为分成小块,并将行为移入相关的对象中
* 动机:将面向过程的代码重构为面向对象的风格。

6.2.7.3. 将领域和表述/显示分离 Separate Domain from Presentation

* 概述:将领域逻辑分离出来,为他们建立独立的领域类
* 动机:从GUI中抽离领域逻辑,从而做到与显示的分离。如MVC模式

6.2.7.4. 提炼继承体系 Extract Hierachy

* 概述:建立继承体系,以一个子类表示一种特殊情况
* 动机:有一个类做了太多的工作,其中一部分是大量的条件表达式完成的。


ps: 大型重构需要较高的对业务场景的抽象能力,使得分类更精确细致,从而提高代码的利用率,使得编码更简单,结构更加清楚。
这样一个细致的体系:
1. 具有很强的对抗变化能力,她将未来所有可能变化的风险,限定在一个子类,甚至一个函数当中;
2. 具有很强的扩展能力,只需要通过实现不同的子类来增加新的功能,既不影响原有结构,更不影响其清晰度。

猜你喜欢

转载自blog.csdn.net/qq_32250495/article/details/86483693