2021年11月更新
一、 容易忽视的问题
⚫ 重复代码
⚫ 过大的类
⚫ 冗赘的元素
⚫ 异曲同工的类
⚫ 注释
1、 Duplicated Code重复代码
不同的地方出现相同的程序结构:
如果你在一个以上的地点看到相同的程序结构,那么可以肯定:设法将它们和而为一,程序会变得更好。最常见的“重复代码”就是一个类内的两个函数含有相同的表达式。另一种常见情况就是两个互为兄弟的子类内含有相同的表达式。
1)同一个类的2个函数含有相同的表达式,这时可以采用Extract Method(提炼函数)提炼出重复的代码,然后让这2个地点都调用被提炼出来的那段代码。
2)两个互为兄弟的子类内含相同表达式,只需对2个类都是用Extract Method(提炼函数),然后对被提炼出来的函数是用Pull Up Method (方法上移) ,将它推入超类。如果代码之间只是类似, 并非完全相同,那么就得运用Extract Method(提炼函数 将相似部分和差异部分隔开,构成单独一个的函数。然后你可能发现可以运用Form Template Method (塑造模板函数)获得一个 Template Method设计模式。如果有些函数以不同的算法做相同的事,你可以选择其中较清晰地一个,并是用 Substitute Algorithm (替换算法)将其他函数的算法替换掉。
如果2个毫不相关的类出现 重复代码,你应该考虑对其中一个运用 Extract Class (提炼类),将重复代码提炼到一个独立类中,然后在另一个类内使用这个新类。但是,重复代码所在的函数可能只应该属于某个类,另一个类只能调用它,抑或这个函数可能属于第三个类,而另2个类应该引用这第三个类。你必须决定这个函数放在哪儿最合适,并确保它被安置后就不会再在其他任何地方出现。
设施
2、 Large Class 过大的类
类不要负责超越本类的职责,即前面提到的单一原则:
如果想利用单个类做太多的事,其内往往就会出现太多实例变量。这样 重复代码(Duplicated Code)就接踵而至了。
运用Extract Class (提炼类)将几个变量一起提炼到新类里。提炼类时应该选择类内彼此相关的变量,将它们放在一起。通常类内的数个变量有相同的前缀或字尾,这就意味有机会把它们提炼到某个组件内。如果这个组件适合作为一个子类,就可以运用 Extract Subclass (提炼子类)。
有时类并非在所有时刻都使用所有实例变量。可多次使用Extract Class (提炼类)和Extract Subclass (提炼子类)
一个类如果拥有太多代码,往往也适合使用Extract Class (提炼类)和Extract Subclass (提炼子类)。技巧:先确定客户端如何使用它们,然后运用 Extract Interface(提炼接口) 为每个使用方式提炼出一个接口。可帮助你看清楚如何分解这个类。
如果是个GUI类,可能需要把数据和行为移到一个独立的领域对象去。可能需要2边各保留一些重复数据,并保持2边同步。Duplicate Observed Data (复制"被监视数据")告诉你该这么做。
3. Lazy Class(冗赘类)
4. Alternative Classes with Different Interfaces(异曲同工的类)
两个类的两个函数做同一件事:
如果两个函数做同一件事,却有着不同的签名,请运用 Rename Method (函数改名)根据它们的用途重新命名。但这往往不够,请反复运用 Move Method (搬移函数)将某些行为移入类,直到2者的协议一致为止。如果你必须反复而赘余的移入代码才能完成这些,或许可运用Extract Superclass (提炼超类)。
例如:A类的接口a,和B类的接口b,做的的是相同一件事,或者类似的事情。我们就把A和B叫做异曲同工的类。可以通过重命名,移动函数,或抽象子类等方式优化
5、 Comments(过多的注释)
我们之所以在这里提到注释,是因为人们常把它当做除臭剂来使用。常常会有这样的情况:你看到一段代码有着长长的注释,然后发现,这些注释之所以存在是因为代码很糟糕。注释可以带我们找到代码中的坏味道。找到坏味道后,我们首先应该以各种重构手法把坏味道去除。完成之后我们常常会发现:注释已经变得多余了,因为代码已经清楚说明了一切。如果你需要注释来解释一块代码做了说明,试试 Extract Method (提炼函数);如果函数已经提炼出来,但还是需要注释来解释其行为,试试 Rename Method (函数改名);如果你需要注释说明某些系统的需求规格,试试 Introduce Assertion (引入断言)。
二、对象函数的问题
⚫ 过长函数
⚫ 过长参数列表
⚫ 基本类型偏执
⚫ 重复的switch
⚫ 循环语句
6、 Long Method 过长函数
函数中的代码行数原则上不要多于100行:
我们遵循这样一条原则:每当感觉需要以注释开说明点什么的时候,我们就需要把说明的东西写进一个独立的函数中,并以其用途(而非实现手法)命名。
90%的场合里,要把函数变小,只需使用Extract Method(提炼函数) ,找到函数中适合集中在一起的部分,将它们提炼出来形成新函数。
如果函数内有大量的参数和临时变量,它们会对你的函数提炼形成障碍。你可以经常运用Replace Temp with Query (以查询取代临时变量),来消除这些临时元素。Introduce Parameter Object (引入参数对象),Preserve Whole Object (保持对象完整)则可以将过长的参数列变得简洁一些。
如果已经这么做了,仍然有太多的临时变量和参数,就应该使用 Replace Method with Method Object (以函数对象取代函数)。如何确定提炼哪一段代码呢?一个很好的技巧是:寻找注释。它们通常指出代码用途和实现手法之间的语义距离。如果代码前方有一行注释,就是在提醒你:可以将这段代码替换成一个函数,而且可以在注释的基础上给这个函数命名。就算只有一行代码,如果它需要以注释来说明,那也值得将它提炼到独立函数中。
条件表达式和循环常常也是提炼的信号。可以使用 Decompose Conditional (分解条件表达式)处理条件表达式。至于循环,应该将循环和其内的代码提炼到独立函数中。(间接层所带来的全部利益---解释能力,共享能力,选择能力--都是由小型函数支持)
7. Long Parameter List 过长参数列
过长参数不易理解和维护:
太长的参宿列难以理解,而且会造成前后不一致,不易使用。而且一旦你需要更多数据,就不得不修改它。如果将对象传递给函数,大多数修改都没有必要。例外:不希望造成被调用对象与较大对象间的某种依赖关系。
如果向已有的对象发出1条请求就可以取代1个参数,那么就运用 Replace Parameter with Methods (以函数取代参数)。在这里,"已有的对象"可能是函数所属类里的1个字段,也可能是另一个参数。还可以运用 Preserve Whole Object (保持对象完整)将来自同一对象的一堆数据收集起来,并以该对象替换它们。如果某些数据缺乏合理的对象归属,可使用 Introduce Parameter Object (引入参数对象)为它们制造出一个参数对象。
重要的例外:如果你明显不想造成"被调用对象"与"较大对象"间的某种依赖关系。这时候将数据从对象中拆解出来单独作为参数,也很合情理。但是请注意其所引发的代价。如果参数列太长或变化太频繁,就需要重新考虑自己的依赖结构了。
实施
8. Primitive Obsession 基本类型偏执
喜欢使用基本类型,而不愿运用小对象: 这里的基本类型,如果指Java语言的话,不仅仅包括那八大基本类型哈,也包括String等。如果是经常一起出现的基本类型,可以考虑把它们封装成对象。
对象的一个极具价值的东西是:它们模糊(甚至打破)了横亘于基本数据和体积较大的类之间的界限。你可以轻松的编写出一些与语言内置(基本)类型无异的小型类。
对象技术的新手通常不愿意在小任务上运用小对象—像是结合数值和币种的money类,由一个起始值和结束值组成的range类等。你可以使用 Replace Data Value withObject (以对象取代数据值)将原本单独存在的数据值替换为对象。如果想要替换的数据值是类型码,而它并不影响行为,则可以运用 Replace Type Code with Class (以类取代类型码)将它替换掉。如果你有与类型码相关的条件表达式,可运用 Replace Type Code with Subclass (以子类取代类型码)或 Replace Type Code with State/Strategy
(以状态/策略取代类型码)加以处理。
如果你有一组应该总是被放在一起的字段,可运用Extract Class (提炼类)。如果你在参数列中看到基本数据类型,不妨试试 Introduce Parameter Object (引入参数对象)。如果你发现自己正从数组中挑选数据,可运用 Replace Array with Object(以对象取代数组).
customer应该是对象,而不是一个字符串,很可能包含其他到属性,后续要用到。
反例如下:
// 订单
public class Order {
private String customName;
private String address;
private Integer orderId;
private Integer price;
}
正例:
// 订单类
public class Order {
private Customer customer;
private Integer orderId;
private Integer price;
}
// 把Customer相关字段封装起来,在Order中引用Customer对象
public class Customer {
private String name;
private String address;
}
不是所有的基本类型,都建议封装成对象,而是有关联或者一起出现的才这么建议。
9. Switch Statement (重复switch )
switch语句的问题在于重复:
面向对象程序的一个最明显特征就是:少用switch或(case)语句。从本质上说,switch语句的问题在于重复。你常会发现switch语句散布于不同地点。如果要为它添加一个新的case子句,就必须找到所有switch语句并修改它们。面向对象中的多态概念可为此带来优雅的解决办法。
大多数时候,一看到switch语句,就应该考虑以多态来替换它。
问题是多态该出现在哪?switch语句常常根据类型码进行选择,你要的是“与该类型码相关的函数或类”,所以应该使用 Extract Method (提炼函数)将switch语句提炼到一个独立函数中,再以 Move Method (搬移函数)将它搬移到需要多态性的那个类里。此时你必须决定是否使用Replace Type Code with Subclass (以子类取代类型码)或 Replace Type Code with State/Strategy (以状态/策略取代类型码)。一旦完成这样继承结构后,就可以运用
Replace Conditional with Polymorphism (以多态取代条件表达式)了。
如果你只是在单一函数中有些选择事例,且并不想改动它们,那么多态就不必要了。这时可运用 Replace Parameter with Explicit Methods (以明确函数取代参数)。如果你的选择条件之一是null,可以试试 Introduce Null Object (引入Null 对象).
用策略模式代替if/switch:主要工厂map,符合开闭原则
三、对象数据的问题
⚫ 神秘命名
⚫ 全局数据
⚫ 可变数据
⚫ 数据泥团
⚫ 临时字段
⚫ 纯数据类
10、神秘命名
主要问题:变量命名错误或者使用魔数的问题:
神秘命名改进目标
重构的目标就是让代码直观明了,让函数、模块、变量和类命名能清晰地表明自己的功能和用法。
11、全局变量
为什么全局变量是坏味道:全局数据是最刺鼻的坏味道之一,像幽灵一般,会带来很多诡异的bug。 全局变量的作用范围是全局,通常指类变量单例等。可以在代码中任一角落修改的数据,而且没有任何有效的机制检测出全局变量修改的位置, 即主要的问题是全局变量在多处地方可以写和读,导致不知道什么时候写,什么时候读。
例1:
java的springmvc,controller层通过@Autowired注解注入:
controller.java:
@Autowired
private UserService userService;UserService.java代码:
private String username;
public boolean saveUsername(String username){
this.content = content;
//业务逻辑处理
}springmvc核心控制器DispatcherServlet 默认为每个controller生成单一实例来处理所有用户请求,所以在这个单一实例的controller中,它的UserService也是一个实例处理所有请求, 这样UserService的成员变量就被所有请求共享。这样就会出现并发请求时变量内容被篡改的问题。
例2: 全局静态变量
public class Constants {
public static String CONST_6 = "6";
}可以在全局任何地方直接修改 Constants.CONST_6 = 7。但由于java是多线程环境,假如再来一个线程也更改这个变量,那么就出问题了。
导致结果:调试代码压力就很大,无法控制变量。全局变量引发的bug,都是一些诡异的bug,难以定位。
重构全局变量的方法:
在开发过程中,我们要尽量避免使用全局变量,如果使用可以通过以下手段来降低它的危险性:
- 封装全局变量:提供对外的get/set接口,外部获取或修改这个全局变量时需要调用接口实现,这样至少我们能知道什么地方修改了它。
- 控制全局变量的作用域:封装好的全局变量 get/set 接口最好放到一个类或模块中,只允许模块内部的代码调用它,从而尽量控制它的作用域。
如果一个全局变量能保证在程序启动后就不会再被修改,那么它还是相对比较安全的,否则就需要非常注意管理访问以及修改这个全局变量的权限。
12、可变数据
对有些数据的随意修改经常会导致出乎意料的结果和难以发现的bug。
x,y 是public,外部可随意修改,应该收回权限,变私有变量。
java中可变数据:
1)过多的setter
setter 的本质是把对象内部的细节交给其他类管理,破坏了封装,所以可能导致行为不可控。
好的解决方案是用一个行为的函数代替setter方法。下面是例子
比如设置状态值,可以是一种行为
另外如果是初始化的过程中进行设值,完全可以用带参的构造函数代替。
有一种重构手法,Remove Setting Method 移除设值函数,做法就是把setter方法完全移除。
Lombok 框架很多公司都不允许使用了。虽然lombok用注解就自动的代替简单的getter和setter ,因为他是在编译阶段产生的,所以源码中没有,但是class文件里面会有对应方法。
除了侵入性太强(跑项目必须安装插件)和可读性可调试性太低,还有一个重要原因就是暴露了不应该的暴露的setter方法。
2)可变的数据
除了字段不包含setter方法
还有一个是设计不变的类,final 修饰的类。
不变类的特征是
- 所有字段只在构造方法中初始化
- 所有的方法都是纯函数
- 如果需要改动,返回一个新的对象,不是修改已有的字段
2. 变量声明和变量赋值分离
编程中有一个基本原则是,变量一次性完成初始化。
例如:改造前后对比:
3、构建不可变的集合:
- 集合是不可变的。
- 无法添加,修改和删除其内部的元素。
- 如果尝试对它们执行添加/删除/更新操作,则会得到
UnsupportedOperationException
下列情况应当避免:
// 避免: 初始化可变列表
List<String> list = new ArrayList<>() {{
add("test01");
add("test02");
add("test03");
}};
// 避免: 在一个方法中改变参数列表的长度
public void change(List<String> list) {
list.add("test04");
}
// 避免: 在一个方法中改变参数列表的内部值
public void fill(List<Model> models) {
models.foreach(model -> model.setType("new_model"));
}
构建不可变的列表:
// 在Java8创建不可修改的列表,使用Collections类提供的静态方法:unmodifiableList()
List<String> list = new ArrayList<>();
list.add("one");
list.add("two");
List<String> immutableList = Collections.unmodifiableList(list);
// Java9以上自带的不可变集合方法
List<String> immutableList = List.of("one","two","three");
// 通过stream来实现列表的合并和过滤,创建一个新的不可变集合
// 两个集合合并
var list_03 = Stream.concat(list_01.stream(), list_02.stream()).collect(Collectors.toUnmodifiableList());
// 多个集合合并
var list_04 = Stream.of(list1.stream(), list2.stream(), list3.stream()).flatMap(Function.identity()).collect(Collectors.toUnmodifiableList());
// 集合过滤
var list_05 = list_04.stream().filter(StringUtils::isNotEmpty).collect(Collectors.toUnmodifiableList());
// 集合改变内部的值生成一个新的集合,这里的model代表一个虚拟的对象
var models = List.of(model1, model2, model3);
var newModels = models.stream().map(model -> model.withType("new_model")).collect(Collectors.toUnmodifiableList());
// ImmutableList是一个不可变、线程安全的列表集合,它只会获取传入对象的一个副本。
ImmutableList<String> list = ImmutableList.of("a", "b", "c", "d");
1)集合搭配Stream可以进行任何变化生成新的不可变集合,没有副作用,非常的nice。
2)ImmutableList是一个不可变、线程安全的列表集合,它只会获取传入对象的一个副本,而不会影响到原来的变量或者对象。ImmutableList创建不可变对象有两种方法,一种是使用静态of方法,另外一种是使用静态内部类Builder。
13、data clumps 数据泥团
众多数据项待在一块: 数据项就像小孩子,喜欢成群结队地呆在一块。如果一些数据项总是一起出现的,并且一起出现更有意义的,就可以考虑,按数据的业务含义分类来封装成数据对象。
常常可以在很多地方看到相同的3、4项数据:2个类中相同的字段、许多函数签名中相同的参数。这些总是绑在一起出现的数据真应该拥有属于它们自己的对象。首先找出这些数据以字段形式出现的地方,运用Extract Class (提炼类)将它们提炼到一个独立对象中。然后将注意力转移到函数签名上,运用Introduce Parameter Object (引入参数对象)或Preserve Whole Object (保持对象完整)为它减肥。这么做的直接好处是可以将很多参数列缩短,简化函数调用。不必在意数据泥团(Data Clumps)只用上新对象的一部分字段,只要以新对象取代2(或更多)个字段,就值得了。
一个好的评判方法是:删除众多数据中的一项。这么做,其他数据有没有因而失去意义?如果它们不再有意义,这就是个明确信号:你应该为它们产生一个新对象。
反例如下:
public class User {
private String firstName;
private String lastName;
private String province;
private String city;
private String area;
private String street;
}
正例:
public class User {
private UserName username;
private Adress adress;
}
class UserName{
private String firstName;
private String lastName;
}
class Address{
private String province;
private String city;
private String area;
private String street;
}
14、 Temporary Field临时字段
对象的暂时性属性经常让人迷惑:某个实例变量仅为某种特定情况而定而设,这样的代码就让人不易理解,我们称之为 Temporary Field(令人迷惑的临时字段)
。
有时你会看到这样的对象:其内某个实例变量仅为某种特定情况而设。这样的代码让人不易理解,因为你通常认为对象在所有时候都需要它的所有变量。在变量未被使用的情况下猜测当初设置目的,会让你发疯。
请使用Extract Class (提炼类)给这些变量创造一个家,然后把所有和这些变量相关的代码都放进这个新家,也许你还可以使用 Introduce Null Object (引入Null 对象)在变量不合法的情况下创建一个null对象,从而避免写出条件式代码。
如果类中有一个复杂算法,需要好几个变量,往往就可能导致坏味道令人迷惑的临时字段(Temporary Field)出现。由于实现者不希望传递一长串参数,所以他把这些参数都放进字段。但是这些字段只在使用该算法时才有效,其他情况下只会让人迷惑。这时可以利用 Extract Class (提炼类)把这些变量和其相关函数提炼到一个独立的类中。提炼后的新对象将是一个函数对象。
再有反例:
public class PhoneAccount {
private double excessMinutesCharge;
private static final double RATE = 8.0;
public double computeBill(int minutesUsed, int includedMinutes) {
excessMinutesCharge = 0.0;
int excessMinutes = minutesUsed - includedMinutes;
if (excessMinutes >= 1) {
excessMinutesCharge = excessMinutes * RATE;
}
return excessMinutesCharge;
}
public double chargeForExcessMinutes(int minutesUsed, int includedMinutes) {
computeBill(minutesUsed, includedMinutes);
return excessMinutesCharge;
}
}
临时字段excessMinutesCharge
是否多余呢?
15. Data Class(纯数据类)
所谓的Data Class是指:它们拥有一些字段,以及用于访问这些字段的函数,除此之外一无长物。这样的类只是不会说话的数据容器,它们几乎一定被其他类过分细琐的操控着。这些类早期可能拥有public字段,果真如此你应该在别人注意到它们之前,立刻运用 Encapsulated Field (封装字段)将它们封装起来。如果这些类含容器类的字段,你应该检查它们是不是得到了恰当的封装;如果没有,就运用 Encapsulated Collection (封装集合)把它们封装起来。对于那些不该被其他类修改的字段,请运用 Remove Setting Method (移除设置函数)。
然后,找出这些取值/设值函数被其他类运用的地点。尝试以 Move Method (搬移函数)把那些调用行为搬移到Data Class来。如果无法搬移这个函数,就运用 Extract Method (提炼函数)产生一个可搬移的函数。不久之后就可以运用 Hide Method (隐藏函数)把这些取值/设值函数隐藏起来了。
Data Class(纯数据类)
所谓Data Class是指:它们拥有一些值域(fields),以及用于访问(读写〕这些值域的函数,除此之外一无长物。这样的classes只是一种「不会说话的数据容器」,它们几乎一定被其他classes过份细琐地操控着。这些classes早期可能拥有public值域,果真如此你应该在别人注意到它们之前,立刻运用Encapsulate Field (封装值域,(就是将public 域变成private然后设置set和get)。将它们封装起来。如果这些classes内含容器类的值域(collection fields),你应该 检査它们是不是得到了恰当的封装;如果没有,就运用 Encapsulate Collection(封装群集:
A method returns a collection.
Make it return a read-only view and provide add/remove methods.
) 把它们封装起来。对于那些不该被其他classes修改的值域,请运用 Remove Setting Method(移除设置函数)。
然后,找出这些「取值/设值」函数(getting and setting methods)被其他classes运用的地点。尝试以Move Method(搬移函数) 把那些调用行为搬移到Data Class来。如果无法搬移整个函数,就运用 Extract Method(提炼函数) 产生一个可被搬移的函数。不久之后你就可以运用Hide Method (隐藏某个函数)把这些「取值/设值」函数隐藏起来了。
特征
纯稚的数据类(Data Class) 指的是只包含字段和访问它们的getter和setter函数的类。这些仅仅是供其他类使用的数据容器。这些类不包含任何附加功能,并且不能对自己拥有的数据进行独立操作。
问题原因
当一个新创建的类只包含几个公共字段(甚至可能几个getters / setters)是很正常的。但是对象的真正力量在于它们可以包含作用于数据的行为类型或操作。
解决方法
如果一个类有公共字段,你应该运用 封装字段(Encapsulated Field) 来隐藏字段的直接访问方式。
如果这些类含容器类的字段,你应该检查它们是不是得到了恰当的封装;如果没有,就运用 封装集合(Encapsulated Collection) 把它们封装起来。
找出这些getter/setter函数被其他类运用的地点。尝试以 搬移函数(Move Method) 把那些调用行为搬移到 纯稚的数据类(Data Class) 来。如果无法搬移这个函数,就运用 提炼函数(Extract Method) 产生一个可搬移的函数。
在类已经充满了深思熟虑的函数之后,你可能想要摆脱旧的数据访问方法,以提供适应面较广的类数据访问接口。为此,可以运用 移除设置函数(Remove Setting Method) 和 隐藏函数(Hide Method) 。
收益
提高代码的可读性和组织性。特定数据的操作现在被集中在一个地方,而不是在分散在代码各处。
帮助你发现客户端代码的重复处。
四、对象关系的问题
⚫ 发散式变化
⚫ 霰弹式修改
⚫ 依恋情结
⚫ 夸夸其谈通用性
⚫ 过长的消息链
⚫ 中间人
⚫ 内幕交易
⚫ 被拒绝的馈赠
16. Divergent Change 发散式变化(针对一个类,相对聚焦式)
定义:
修改一个类需要需要很多处代码.因为修改的代码散布四处,即一个类因为多个原因被修改,类的功能过多做了不止一件事。
对程序进行维护时, 如果添加修改组件, 要同时修改一个类中的多个方法, 那么这就是 Divergent Change。
发散式变化的原因:
如果在系统需要修改的时候不能做到只在某一点出做修改,应该意识到代码是否过于紧密耦合的味道了:比如某个类新加入一个功能,需要修改这三个函数,新加入另一个功能需要修改这四个函数,这时候把这个类按照变化方向分成两个类比较好。
例如男人是 “聚焦式认知” 女人是“发散式认知”.简单点说,聚焦式就是把大面凝聚成一个点,发散式相反它是把一个点扩展成大面
男人可以把几个问题的共性找出来,再再想办法加以解决。男人以解决问题为目的;女人可以把一个问题分拆出好几个方向去诉说,永远不想解决的操作办法。女人以诉说问题为目的。
发散式变化重构方法:拆分类提炼出新类。
如果某个类经常因为不同的原因在不同的方向上发生变化,发散式变化(Divergent Change)就出现了。那么此时也许将这个类分成2个会更好,这么一来每个对象就可以只因1种变化而需要修改。针对某以外界变化的所有相应修改,都只应该发生在单一类中,而这个新类内的所有内容都应该反应此变化。为此应该找出某特定原因而造成的所有变化,然后运用Extract Class (提炼类)将它们提炼到另一个类中。
举个汽车的例子,某个汽车厂商生产三种品牌的汽车:BMW、Benz和LaoSiLaiSi,每种品牌又可以选择燃油、纯电和混合动力。反例如下:
/**
* 公众号:捡田螺的小男孩
*/
public class Car {
private String name;
void start(Engine engine) {
if ("HybridEngine".equals(engine.getName())) {
System.out.println("Start Hybrid Engine...");
} else if ("GasolineEngine".equals(engine.getName())) {
System.out.println("Start Gasoline Engine...");
} else if ("ElectricEngine".equals(engine.getName())) {
System.out.println("Start Electric Engine");
}
}
void drive(Engine engine,Car car) {
this.start(engine);
System.out.println("Drive " + getBrand(car) + " car...");
}
String getBrand(Car car) {
if ("Baoma".equals(car.getName())) {
return "BMW";
} else if ("BenChi".equals(car.getName())) {
return "Benz";
} else if ("LaoSiLaiSi".equals(car.getName())) {
return "LaoSiLaiSi";
}
return null;
}
}
如果新增一种品牌新能源电车,然后它的启动引擎是核动力呢,那么就需要修改Car类的start
和getBrand
方法啦,这就是代码坏味道:Divergent Change (发散式变化)。
如何优化呢?一句话总结:拆分类,将总是一起变化的东西放到一块。
★”
- 运用提炼类(Extract Class) 拆分类的行为。
- 如果不同的类有相同的行为,提炼超类(Extract Superclass) 和 提炼子类(Extract Subclass)。
正例如下:
因为Engine是独立变化的,所以提取一个Engine接口,如果新加一个启动引擎,多一个实现类即可。如下:
//IEngine
public interface IEngine {
void start();
}
public class HybridEngineImpl implements IEngine {
@Override
public void start() {
System.out.println("Start Hybrid Engine...");
}
}
因为drive
方法依赖于Car,IEngine,getBand
方法;getBand
方法是变化的,也跟Car是有关联的,所以可以搞个抽象Car的类,每个品牌汽车继承于它即可,如下
public abstract class AbstractCar {
protected IEngine engine;
public AbstractCar(IEngine engine) {
this.engine = engine;
}
public abstract void drive();
}
//奔驰汽车
public class BenzCar extends AbstractCar {
public BenzCar(IEngine engine) {
super(engine);
}
@Override
public void drive() {
this.engine.start();
System.out.println("Drive " + getBrand() + " car...");
}
private String getBrand() {
return "Benz";
}
}
//宝马汽车
public class BaoMaCar extends AbstractCar {
public BaoMaCar(IEngine engine) {
super(engine);
}
@Override
public void drive() {
this.engine.start();
System.out.println("Drive " + getBrand() + " car...");
}
private String getBrand() {
return "BMW";
}
}
细心的小伙伴,可以发现不同子类BaoMaCar和BenzCar的drive
方法,还是有相同代码,所以我们可以再扩展一个抽象子类,把drive
方法推进去,如下:
public abstract class AbstractRefinedCar extends AbstractCar {
public AbstractRefinedCar(IEngine engine) {
super(engine);
}
@Override
public void drive() {
this.engine.start();
System.out.println("Drive " + getBrand() + " car...");
}
abstract String getBrand();
}
//宝马
public class BaoMaRefinedCar extends AbstractRefinedCar {
public BaoMaRefinedCar(IEngine engine) {
super(engine);
}
@Override
String getBrand() {
return "BMW";
}
}
如果再添加一个新品牌,搞个子类,继承AbstractRefinedCar
即可,如果新增一种启动引擎,也是搞个类实现IEngine
接口即可
17. Shotgun Surgery 霰弹式修改(针对多个类)
定义:
仅做一个简单的修改却要修改多个类。即我们需要修改的代码散布四处:
Shotgun Surgery 霰弹式修改 (仅做一个简单的修改却要求改变多个类。即我们需要修改的代码散布四处,不但很难找到它们,也很容易忘掉某个重要的修改。 )
霰弹式修改跟发散式变化(Divergent Change) 的区别就是,它指的是同时对多个类进行单一的修改,发散式变化指在一个类中修改多处。
霰弹式修改的原因:
1、类之间耦合过重或者相互依赖太多。将多个类相分离是代码的一大职责。有可能缺少一个通晓全盘职责的类 (而大量修改本应针对这个类完成)。
2、另外,也有可能因过度去除发散式改变而招致这个坏味道。 动时,我们称之为霰弹式修改。这通常是因为
霰弹式修改重构方法:
找出一个应对这些修改负责的类。这可能是一个现有的类,也可能需要通过应用抽取类来创建一个新的类。使用搬移字段(Move Field)和搬移方法(Move Method),将功能置于所选的类中。如果未选中类足够简单,则可以使用内联类(Inline Class)将该类去除。
反例如下:
public class DbAUtils {
@Value("${db.mysql.url}")
private String mysqlDbUrl;
...
}
public class DbBUtils {
@Value("${db.mysql.url}")
private String mysqlDbUrl;
...
}
多个类使用了db.mysql.url
这个变量,如果将来需要切换mysql
到别的数据库,如Oracle
,那就需要修改多个类的这个变量!
如何优化呢?将各个修改点,集中到一起,抽象成一个新类。
★可以使用 Move Method (搬移函数)和 Move Field (搬移字段)把所有需要修改的代码放进同一个类,如果没有合适的类,就去new一个。
”
正例如下:
public class DbUtils {
@Value("${db.mysql.url}")
private String mysqlDbUrl;
...
}
18. Feature Envy 依恋情节
定义: 类的方法应该去该去的地方:
即某个函数为了计算某值,从另一个对象那儿调用了大量的取值函数。或者需要大量访问另外Class中的成员变量来计算值。
函数对某个类的兴趣高过对自己所处的类,通常的焦点就是数据,某个函数为了计算某个值,从另一个对象那儿调用几乎半打的取值函数。通俗点讲,就是一个函数使用了大量其他类的成员,有人称之为红杏出墙的函数。
依恋情结的原因:
违反来将数据和操作行为(方法)包装在一起的原则。
重构方法:
这时一个运用 Move Method (搬移函数)把它移到自己对应的Class中。有时候函数中只有一部分受这种依恋之苦,这时候使用Extract Method (提炼函数)把这部分提炼到独立函数中,再使用Move Method (搬移函数)带它去它的梦中家园。
一个函数往往会用到几个类的功能,那么它究竟该被置于何处呢?我们的原则是:判断哪个类拥有最多被此函数使用的数据,然后就把这个函数和那些数据摆在一起。
现象:Class中某些方法“身在曹营心在汉”,没有安心使用Class中的成员变量,而需要大量访问另外Class中的成员变量。这样就违反了对象技术的基本定义:将数据和操作行为(方法)包装在一起。
反例如下:
public class User{
private Phone phone;
public User(Phone phone){
this.phone = phone;
}
public void getFullPhoneNumber(Phone phone){
System.out.println("areaCode:" + phone.getAreaCode());
System.out.println("prefix:" + phone.getPrefix());
System.out.println("number:" + phone.getNumber());
}
}
如何解决呢?在这种情况下,你可以考虑将这个方法移动到它使用的那个类中。例如,要将 getFullPhoneNumber()
从 User
类移动到Phone
类中,因为它调用了Phone
类的很多方法。
19. Speculative Generality(夸夸其谈通用型/未来性)
夸夸其谈通用性:遵循合适原则,不用为未来的无限的可能情况做设计。
尽量避免过度设计的代码。例如:
- 不必要的类:如果某个抽象类没有什么太大的作用,就运用
Collapse Hierarchy
(折叠继承体系) - 不必要的参数:如果函数的某些参数没用上,就移除。
- 只有一个if else,那就不需要班门弄斧使用多态;
- 如果函数名称带有多余的抽象意味,应该对它实施Rename Method (函数改名)
如果函数或类的唯一用户是测试用例,这就飘出了坏味道 夸夸其谈未来性(Speculative Generality)。 如果有这样的函数或类,请把它们连同其测试用例一并删除。但如果它们的用途是帮助测试用例检测正当功能,则不能删除。
20. Message Chains(过度耦合的消息链/过长的消息链)
A对象请求B对象,B对象请求C对象...:
如果你看到用户向一个对象请求另一个对象,然后再向后者请求另一个对象,然后再请求另一个对象……这就是消息链。实际代码中你看到的可能是一长串getThis()或一长串临时变量。采取这种方式,意味客户代码将与查找过程中的导航紧密耦合。一旦对象间关系发生任何变化,客户端就不得不做出相应的修改。
反例:
A.getB().getC().getD().getTianLuoBoy().getData();
A想要获取需要的数据时,必须要知道B,又必须知道C,又必须知道D...
其实A需要知道得太多啦,回头想下封装性。其实可以通过拆函数或者移动函数解决,比如由B作为代理,搞个函数直接返回A需要数据。
这时候应该使用 Hide Delegate (隐藏委托关系)。你可以在消息链的不同位置进行这种重构。理论上可以重构消息链上任何对象,但这么做往往会把一系列对象都变成Middle Man(中间人)。通常更好的选择是:先观察消息链最终得到的对象是用来干什么的,看看能否以 Extract Method (提炼函数)把使用该对象的代码提炼到一个独立函数中,再运用 Move Method (搬移函数)把这个函数推入消息链。
过度运用委托:
对象的基本特征之一就是封装:对外部世界隐藏其内部细节。封装往往伴随委托。比如说你问你主管是否有时间参加一个会议,他就把这个消息“委托”给他的记事簿,然后才能回答你。你没必要知道这位主管到底使用传统记事簿或电子记事簿或秘书来记录自己的约会。
反例:
A.B.getC(){
return C.getC();
}
其实,A可以直接通过C去获取C,而不需要通过B去获取。
但是人们可能过度运用委托。你也许会看到某个类有一半的函数都委托给其他类,这样就是过度运用。这时应该使用Remove Middle Man (移除中间人),直接和真正负责的对象打交道。如果不干实事的函数只有少数几个,可以运用 Inline Method (内联函数)把它们放进调用端。如果这些中间人还有其他行为,可以运用 Replace Delegation with Inheritance (以继承取代委托)把它们变成实责对象的子类,这样你即可以扩展原对象的行为,又不必负担那么多的委托动作。
22. 内幕交易
23、Refused Bequest(被拒绝的遗赠)
子类仅仅使用父类中的部分方法和属性。其他来自父类的馈赠成为了累赘。
子类应该继承超类的函数和数据。但如果它们不想或不需要继承,又该这么办呢?它们得到所有礼物,却只从中挑选几样来玩。
按传统说法,这就意味着继承体系设计误。你需要为这个子类新建一个兄弟类,再次运用 push down Method (函数下移)和 push down field (字段下移)把所有用不到的函数下推给那个兄弟。这样一来,超类就只持有所有子类共享的东西。你常常会听到这样的建议:所有超类都应该是抽象的。既然使用“传统说法”这个略带贬义的词,你就可以猜到,我们不建议你这么做,起码不建议你每次都这么做。我们继承利用继承来复用一些行为,并发现可以很好的应用于日常工作。这也是一种坏味道,我们不否认,但气味通常并不强烈。所以我们说:如果Refused Bequest引起困惑和问题,请遵循传统忠告。
但不必认为你每次都得那么做。十有八九这种坏味道很淡,不值得理睬。如果子类复用了超类的行为,却有不愿支持超类的接口,Refused Bequest的坏味道就会变得浓烈。拒绝继承超类的实现, 这点我们不介意;但如果拒绝继承超类的接口,我们不以为然。不过即使你不愿意继承接口,也不要胡乱修改继承体系,应用运用 Replace Inheritance with Delegation (以委托取代继承)来达到目的。
24、Parallel Inheritance Hierarchies 平衡继承体系
平行继承体系其实是散弹式修改(Shotgun Surgery)的特殊情况:
在这种情况下,每当你为某个类增加1个子类,必须也为另一个类相应增加1个子类。如果你发现某个继承体系的类名前缀和另一个继承体系的类名前缀完全相同,便是闻到了这种坏味道。
消除这种重复性的一般策略是:让一个继承体系的实例引用另一个继承体系的实例。如果再接再厉运用 Move Method (搬移函数)和Move Field (搬移字段),,就可以将引用端的继承体系取消。
设施
25. Inappropriate Intimacy(过于亲密关系/狎昵关系)
2个类过于亲密: 如果两个类过于亲密,过分狎昵,你中有我,我中有你,两个类彼此使用对方的私有的东西,就是一种坏代码味道。我们称之为Inappropriate Intimacy(狎昵关系)
建议尽量把有关联的方法或属性抽离出来,放到公共类,以减少关联:
有时候你会看到2个类过于亲密,花费太多时间起探究彼此的private成分。你可以采用Move Method (搬移函数)和 Move Field (搬移字段)帮他们划清界限。你也可以看看是否可以运用 Change Bidirectional Association to Unidirectional (将双向关联改为单向关联)让其中一个类对另一个斩断情丝。如果2个类实在是情投意合,可以运用Extract Class (提炼类)把2者共同点提炼到一个安全地点,让它们坦荡的使用这个新类。或者可以尝试运用 Hide Delegate (隐藏委托关系)让另一个类来为它们传递相思情。
继承往往造成过度亲密,因为子类对超类的了解总是超过后者的主观愿望,如果你觉得该让这个孩子独自生活了,请运用Replace Inheritance with Delegation (以委托取代继承)让它离开继承关系。
五、重构例子
在Clean Code的3.4节中有这样一段代码(代码清单3-4)。(第3章主要讲的是函数,而3.4节讨论的是switch
语句。)
public Money calculatePay(Employee e) throws InvalidEmployeeType {
switch (e.type) {
case COMMISSIONED:
return calculateCommissionedPay(e);
case HOURLY:
return calculateHourlyPay(e);
case SALARIED:
return calculateSalariedPay(e);
default:
throw new InvalidEmployeeType(e.type);
}
}
这段代码的几处问题:
1、违反了单一权责原则:明显做了不止一件事。
2、违反了开放闭合原则,因为每当添加新类型时,就必须修改之。每当新增加一种类型的Employee
,就必须在这个switch
里添加新的case
。
3、发散式变化:这个方法为三种类型的Employee
计算费用,任何一种类型的计算逻辑发生变化,都要修改这个计算方法(或者对应的计算方法)。
calculatePay(Employee e)
这个方法所在的类中还会有类似isPayDay(Employee e, Date date)
或deliverPay(Employee e, Money pay)
这样的方法,由于Employee
具有不同的类型(type
),那么这些方法中肯定也会出现类似的switch
语句。
1、第一个解决方案:
将switch语句埋到抽象工厂底下,不让任何人看到。该工厂使用switch语句为
Employee
的派生物创建适当的实体,而不同的函数,如calculatePay
、isPayday
和deliverPay
等,则藉由Employee
接口多态地接受派遣。
哪里来的抽象工厂?Employee
怎么就有了派生类?再一看代码
public abstract class Employee {
public abstract boolean isPayday();
public abstract Money calculatePay();
public abstract void deliverPay(Money pay);
}
public interface EmployeeFactory {
public Employee makeEmployee(EmployeeRecord r) throws InvalidEmployeeType;
}
public class EmployeeFactoryImpl implements EmployeeFactory {
public Employee makeEmployee(EmployeeRecord r) throws InvalidEmployeeType {
switch (r.type) {
case COMMISSIONED:
return new CommissionedEmployee(r) ;
case HOURLY:
return new HourlyEmployee(r);
case SALARIED:
return new SalariedEmploye(r);
default:
throw new InvalidEmployeeType(r.type);
}
}
}
2、识别坏代码的味道:
这里的坏味道很多,最一目了然的可能就是”Switch Statements(Switch惊悚现身)“,当然还有Bob大叔指出的违反SRP和OCP,但在坏味道中,我们管它叫做”Divergent Change(发散式变化)“。然而要消除这两个坏味道都不是那么容易,我只想从最简单的开始。什么是最简单的呢?
首先calculatePay
方法有一个Employee
类型的参数,除此之外,它没有使用该方法所在类的任何成员。同理可以推断该方法所使用的其他三个方法应该也只是使用Employee
,不会使用该方法所在类的成员。这时,”Feature Envy(依恋情结)“的坏味道就显现出来了。
有一种经典气味是:函数对某个类的兴趣高过对自己所处类的兴趣。这种孺慕之情最通常的焦点便是数据。无数次经验里,我们看到某个函数为了计算某个值,从另一个对象那儿调用几乎半打的取值函数。
对于该方法,岂止是半打,简直就是全部。老马不但指出了问题,还给出了方案:
疗法显而易见:把这个函数移至另一个地点。你应该使用"Move Method(搬移函数)把它移到它该去的地方”
将calculateCommisstionPay
、calculateHourlyPay
、calculateSalariedPay
方法,以及它自己全部挪到Employee
中(常用IDE都支持重构,这里不再赘述):
public Money calculatePay() throws InvalidEmployeeType {
switch (this.type) {
case COMMISSIONED:
return this.calculateCommissionedPay();
case HOURLY:
return this.calculateHourlyPay();
case SALARIED:
return this.calculateSalariedPay();
default:
throw new InvalidEmployeeType(this.type);
}
}
这样一来,该方法只使用Employee
自己的数据,”依恋情结“的坏味道不见了。
是祸躲不过,Switch这个坏味道早晚得面对。
从本质上说,switch语句的问题在于重复。你常会发现同样的switch语句散布于不同地点。如果要为它添加一个新的case子句,就必须找到所有switch语句并修改它们。
由于我们已经通过消除”依恋情结“将方法移动到了Employee
中,所以接下来只需要从类型码入手就好了。这里我们选择使用Replace Type Code with Subclass(以子类取代类型码)这个重构手法。
abstract int getType();
abstract Money calculatePay();
static Employee create(int type) {
switch (type) {
case COMMISSIONED:
return new CommissionedEmployee();
case HOURLY:
return new HourlyEmployee();
case SALARIED:
return new SalariedEmployee();
default:
throw new InvalidEmployeeType(type);
}