1.1 何为重构,为何重构
第一个定义是名词形式:
重构(名词):对软件内部结构的一种调整,目的是在不改变「软件可察行为」前提下,提高其可理解性,降低修改成本。
「重构」的另一个用法是动词形式:
重构(动词):使用一系列重构准则手法,在不改变「软件可察行为」前提 下,调整其结构。
-
改进软件设计,使软件更易被理解。
ps:重构是一种经济适用行为,而非道德使然,如果它不能让我们更快更好的开发,那么它是毫无意义。
-
重构对个体程序员的意义是提高ROI。
- 更快速的定位问题,节省调试时间。
- 最小化变更风险,提高代码质量,减少修复事故的时间。
- 得到程序员同行的认可,更好的发展机会。
-
重构对整个研发团队的意义是战斗力的提升。
1.2 什么时候需要重构?
三次法则;
添加功能更时重构;
修补错误时重构;
复审代码时重构;
- Code review : 在给别人code review时嗅出坏味道,在不失礼貌的前提下提出建议。
- 每次 commit 代码时: 每一次经你之手提交的代码都应该比之前更加干净。
- 当你接手一个异常难读的项目时: 说服项目组将重构作为一项需求任务来做。
- 当迭代效率低于预期时: 将重构当作一个项任务专门来做,必要的时候停下来迭代需求。
重构过程中关于两顶帽子的比喻:
使用重构技术开发软件时,你把自己的时间分配给两种截然不同的行为:「添加新功能」和「重构」。
添加新功能时,你不应该修改既有代码,只管添加新功能。重构时你就不能再添加功能,只管改进程序结构。
软件开发过程中,你可能会发现自己经常变换帽子。首先你会尝试添加新功能,然后觉得把程序结构改一下,功能的添加会容易得多。于是你换一顶帽 子,做一会儿重构工作。接着重复该过程。整个过程或许只花十分钟,但无论何时你都应该清楚自己戴的是哪一顶帽子。
二 代码/架构的坏味道
何时重构?书中告诉了你一些迹象,它会指出「这里有一个可使用重构解决的问题」。
详细可参考:导图,篇幅所限,下面举例了其中15条,重点在介绍这种“坏味道”,相应的应对方法可以参考:https://www.itzhai.com/articles/bad-code-small.html
2.1 Mysterious Name(神秘命名)
好的名字能节省未来用在猜谜上的大把时间。
源代码:
function getPrice(order) {
const a = order.quantity * order.itemPrice
const b = Math.max(0, order.quantity - 500) * order.itemPrice * 0.05
const c = Math.min(basePrice * 0.1, 100)
return a - b + c
}
改进:
function getPrice(order) {
// 获取基础价格
const basePrice = order.quantity * order.itemPrice
// 获取折扣
const quantityDiscount = Math.max(0, order.quantity - 500) * order.itemPrice * 0.05
// 获取运费
const shipping = Math.min(basePrice * 0.1, 100)
// 计算价格
return basePrice - quantityDiscount + shipping
}
2.2 Duplicated Code(重复的代码)
“如果你在一个以上的地点看到相同的代码结构,那么可以肯定:设法将它们合而为一,程序会变得更好。一旦有重复代码存在,阅读这些重复的代码时你就必须加倍仔细,留意其间细微的差异。如果要修改重复代码,你必须找出所有的副本来修改。”
2.3 Long Method(过长函数)
“据我们的经验,活得最长、最好的程序,其中的函数都比较短。初次接触到这种代码库的程序员常常会觉得“计算都没有发生”——程序里满是无穷无尽的委托调用。但和这样的程序共处几年之后,你就会明白这些小函数的价值所在。间接性带来的好处——更好的阐释力、更易于分享、更多的选择——都是由小函数来支持的。”
2.4 Large Class(过大类)
2.5 Long Parameter List(过长参数列)
“刚开始学习编程的时候,老师教我们:把函数所需的所有东西都以参数的形式传递进去。这可以理解,因为除此之外就只能选择全局数据,而全局数据很快就会变成邪恶的东西。但过长的参数列表本身也经常令人迷惑。”
public class LongParameterListExample {
public void processData(String name, String address, int age, String gender, String occupation, String phoneNumber, String email) {
// process data here
}
}
上面的代码定义了一个方法processData
,它有7个参数。在这个例子中,我们可以发现参数列非常长,不利于程序的可读性和可维护性。这是一个典型的过长参数列的例子。
2.6 Divergent Change(发散式变化)
指一个类受多种变化的影响。
你发现你想要修改一个函数,却必须要同时修改许多不相关的函数。例如,当你想要添加一个新的产品类型时,你需要同步修改对产品进行查找、显示、排序的函数。
2.7 Shotgun Surgery(霰弹式修改)
多种变化引发多个类相应的修改。
任何修改都需要在许多不同类上做小幅度修改。
可能原因:一个单一的职责被拆分成大量的类。
注意霰弹式修改 与 发散式变化 区别 : 发散式变化是在一个类受多种变化影响, 每种变化修改的方法不同, 霰弹式修改是 一种变化引发修改多个类中的代码。
2.8 Feature Envy(依恋情结)
函数大量地使用了另外类的数据。这种情况下最好将此函数移动到那个类中。
函数对某个class的兴趣高过对自己所处之 class的兴趣。无数次经验里,我们看到某个函数为了计算某值,从另一个对象那儿调用几乎一半以上的取值函数。 影响:数据和行为不在一处,修改不可控。 解决方案:让数据和行为在一起,通过 Extract Method(提炼函数)和Move Method(搬移函数)的方法来处理,这函数到该去的地方。
2.9 Data Clumps(数据泥团)
数据泥团指的是经常一起出现的数据,比如每个方法的参数几乎相同,处理方式与过长参数列的处理方式相同,用Introduce Parameter Object(引入参数对象)将参数封装成对象。
2.10 Primitive Obsession(基本型别偏执)
写代码时总喜欢用基本类型来当参数,而不喜欢用对象。当要修改需求和扩展功能时,复杂度就增加了。
2.11 Lazy Element(冗赘的元素)
去除多层不必要的包装。
如:方法a中包的是b,b包的是c,c包的是d。但是bc只是基于某种考虑的纯粹包装,而从未有其他变化,这时可以让a直接包d,bc就去掉吧。
class Customer {
private String name;
private String address;
private String city;
private String state;
private String zip;
private String phone;
private String email;
public Customer(String name, String address, String city, String state,
String zip, String phone, String email) {
this.name = name;
this.address = address;
this.city = city;
this.state = state;
this.zip = zip;
this.phone = phone;
this.email = email;
}
public String getEmail() {
return email;
}
}
class Order {
private Customer customer;
private int total;
public Order(Customer customer, int total) {
this.customer = customer;
this.total = total;
}
public String getCustomerEmail() {
return customer.getEmail();
}
}
在上面的代码中,我们定义了一个Order
类和一个Customer
类,其中Order
类知道Customer
类的详细信息,但仅使用Customer
的电子邮件。这是一个冗赘的元素,因为只需要知道用户的电子邮件,但是却存储了大量未使用的数据。在这种情况下,重构可能会改为:
class CustomerEmail {
private String email;
public CustomerEmail(String email) {
this.email = email;
}
public String getEmail() {
return email;
}
}
class Order {
private CustomerEmail customerEmail;
private int total;
public Order(CustomerEmail customerEmail, int total) {
this.customerEmail = customerEmail;
this.total = total;
}
public String getCustomerEmail() {
return customerEmail.getEmail();
}
}
现在,我们只存储所需的信息,而不是冗赘的信息,这样可以使代码更简洁。
2.12 Message Chains(过长的消息链)
向一个对象请求另一个对象,然后再向后者请求另一个对象,然后再请求另一个对象……
未充分的考虑数据结构的读取场景,导致在需要使用某些数据的时候无法简单的获得其引用,或者为了使用某个字段,需要了解一堆中间封装的数据结构。
a.b.c.d.e()
2.13 Middle Man(中间人)
对象的基本特征之一就是封装——对外部世界隐藏其内部细节。封装往往伴随着委托。比如,你问主管是否有时间参加一个会议,他就把这个消息“委托”给他的记事簿,然后才能回答你。很好,你没必要知道这位主管到底使用传统记事簿还是使用电子记事簿抑或是秘书来记录自己的约会。
但是人们可能过度运用委托。你也许会看到某个类的接口有一半的函数都委托给其他类,这样就是过度运用。这时应该使用移除中间人,直接和真正负责的对象打交道。如果这样“不干实事”的函数只有少数几个,可以运用内联函数把它们放进调用端。如果这些中间人还有其他行为,可以运用以委托取代超类或者以委托取代子类把它变成真正的对象,这样你既可以扩展原对象的行为,又不必负担那么多的委托动作。
2.14 Refused Bequest(被拒绝的遗赠)
子类应该继承超类的函数和数据。但如果它们不想或不需要继承,又该怎么办呢?它们得到所有礼物,却只从中挑选几样来玩!
按传统说法,这就意味着继承体系设计错误。你需要为这个子类新建一个兄弟类,再运用函数下移和字段下移把所有用不到的函数下推给那个兄弟。这样一来,超类就只持有所有子类共享的东西。你常常会听到这样的建 议:所有超类都应该是抽象(abstract)的。
2.15 注释(Comments)
- 废话注释;
- 与代码逻辑不一致的注释;
- 尽量让提炼的函数和精炼易懂的命名减少注释的必要;
参考资料
https://refactoringguru.cn/
速看笔记版
https://www.itzhai.com/articles/refactoring-cheat-sheet.html
https://www.itzhai.com/articles/bad-code-small.html
《重构》笔记—坏代码的味道与处理
坏味道与重构手法速查表