关于“重构”的一些思考

简介: 本文将从一个新人数次修改CR comments的角度探讨代码重构的定义、目的以及常见的重构方法,并以简单的代码案例来说明代码重构的具体实现。

任何一个傻瓜都可以写出计算机可以理解的代码,唯有写出人类容易理解的代码,才是优秀的程序员。

                                                                                                                                                         ——Martin Fowler 《重构》


讲一个小故事

最近公司的电梯出了问题,平常从1层到7层都畅通无阻的电梯,偏偏在经过4楼时神秘的跳过了这一层。老板决定由你全盘负责电梯的修理和维护。你简单研究了下这部电梯,似乎初步找出了问题的关键所在。这部电梯的1层到4层是一个老程序员修建的,而后面的5到7层则来自于另一位跟你水平相仿的程序员。这也就是说,问题很大可能正是来自于4层到5层的接缝处,随着公司大楼的不断加高,原本生效的程序发生了某些错误的吻合,才导致4层被神秘的“跳过”。了解了问题所在,你立刻开始针对性的开展工作,在你的妙手回春下,你和你的老板一起眼睁睁的看着电梯不可逆转的上升,向上、向上,甚至还顶破了7层的天花板,在一众员工的众目睽睽下消失在了高空之中……

你开始意识到问题可能超出了你的想象,面对老板的不断催促,你只能赶鸭子上架般拿出了你的最终方案:

  1. 1.要求前往4楼的人站在电梯的后半部分。
  2. 2.在电梯下方安装滑梯
  3. 3.将电梯后半部分的地板换成活版门,当检测到有人按4楼时,在5楼开启活版门
  4. 4.最重要的一步,关闭电梯内部的光源,将电梯变成后来人看不明白的黑箱,也就没人会来找你的麻烦了。

虽然有些员工抱怨前往4楼时会遇到匪夷所思的失重感,但这无伤大雅的问题跟4楼的恢复使用又能算得上什么呢?你唯一需要知道的就是,你成功的解决了公司的问题,你的薪水翻了三番,而且你因为业务能力出色被调到了跟程序毫无关系的领导岗位,甚至还跟前一任修理电梯的员工当上了同事。

至于你问那个电梯?要相信后人的智慧。


前言

上文的小故事生动地向我们展现了重构在系统中的重要性,在软件开发领域,代码重构(Code Refactoring)是一项十分重要的技术。不仅可以提高代码的可读性和可维护性,同时也可以提高代码的质量和性能。本文将从一个新人数次修改CR comments的角度探讨代码重构的定义、目的以及常见的重构方法,并以简单的代码案例来说明代码重构的具体实现。


一、代码重构的定义

代码重构是指在不改变代码功能的前提下,通过修改代码的内部结构和外部表现形式,来提高代码的可读性、可维护性、性能和可扩展性的一种技术。代码重构通常包括以下几个方面:

  1. 1.改进代码的结构,使代码更加清晰简洁;
  2. 2.消除代码中的重复部分,减少代码冗余;
  3. 3.提高代码的可读性,使代码更加易于理解和维护;
  4. 4.提高代码的性能,减少代码的执行时间和内存占用;
  5. 5.改善代码的可扩展性,使代码更容易被扩展和修改。

二、代码重构的目的

代码重构的主要目的是提高代码的质量,使其更加易于理解、维护和扩展。具体来说,代码重构的目的包括以下几个方面:


  1. 1.提高代码的可读性

可读性是衡量代码质量的重要指标之一。鲁肃曾言:写下一行代码只要1分钟,但未来会被一代代工程师读很多次、改很多次。代码的可读性与可维护性,是我心目中好代码的第一标准。

良好的可读性可以使代码更加易于理解和维护,减少代码的错误和bug。代码重构可以通过改进代码的结构、消除代码中的冗余部分等方式来提高代码的可读性。


2.减少代码的冗余

冗余代码是指在代码中重复出现的部分。最单纯的重复代码就是“同一个类的两个函数含有相同的表达式”,冗余代码会使代码量增大,影响代码的可读性和可维护性。代码重构可以通过消除代码中的冗余部分来减少代码量,提高代码的可维护性。


  1. 3.提高代码的性能

代码重构可以通过优化代码结构和算法来提高代码的性能。具体来说,可以通过减少代码的执行时间和内存占用来提高代码的性能。虽然重构也有可能是软件的运行速度下降,但重构之后也使软件的性能优化更加容易,长时间看,最终的效果还是好的。


  1. 4.提高代码的可扩展性

可扩展性是指代码在未来可以被容易地修改和扩展。代码重构可以通过改进代码结构和使用设计模式等方式来提高代码的可扩展性。这个就是代码设计方面的问题了。


三、代码重构的方法

代码重构的方法有很多种,从顶层设计到底层逻辑均可以实现重构,然而,若是所有的人力都投入到技术改造上,可能距离拥抱变化也就不远了。从日常开发角度看,下面列举笔者在开发过程中用到的几种最为常见的方法,代码较为简单,主要是体会重构的思路。


方法提取

这种重构方法是我在开发过程中最常用的一个方法,因为我会经常由于一个方法过长被提了若干个CR comments,团队内的一个前辈告诉我,一个方法不宜超过50行,超过50行的代码,就充斥着“代码坏味道”。

方法提取是指将一段代码抽象出来形成一个方法。这样做的好处是可以减少代码的重复,提高代码的可读性和可维护性。下面用一个案例来说明提取方法的具体实现。

重构前的代码:

publicvoidprintInvoice(Invoiceinvoice) {
System.out.println("Invoice Number: "+invoice.getNumber());
System.out.println("Customer Name: "+invoice.getCustomer().getName());
System.out.println("Invoice Date: "+invoice.getDate());
System.out.println("Total Amount: "+invoice.getTotalAmount());
System.out.println("Items:");
for (InvoiceItemitem : invoice.getItems()) {
System.out.println(item.getName() +" - "+item.getPrice() +" - "+item.getQuantity());
    }
}

上述代码的printInvoice()方法输出的内容五花八门,打印发票的过程包括了打印头部信息和打印详细项目信息,没有一个清晰的定义,所以需要对上述代码进行重构。根据方法抽取的原则,将打印头部信息和打印详细项目信息分别抽象成两个方法,使代码更加清晰简洁。

重构后的代码:

publicvoidprintInvoice(Invoiceinvoice) {
printInvoiceHeader(invoice);
printInvoiceItems(invoice.getItems());
}
privatevoidprintInvoiceHeader(Invoiceinvoice) {
System.out.println("Invoice Number: "+invoice.getNumber());
System.out.println("Customer Name: "+invoice.getCustomer().getName());
System.out.println("Invoice Date: "+invoice.getDate());
System.out.println("Total Amount: "+invoice.getTotalAmount());
}
privatevoidprintInvoiceItems(List<InvoiceItem>items) {
System.out.println("Items:");
for (InvoiceItemitem : items) {
System.out.println(item.getName() +" - "+item.getPrice() +" - "+item.getQuantity());
    }
}

重构后的代码更加清晰,可读性明显提升不少。在了解这个方法之前,我曾经有过一个疑惑:会不会因为短函数的原因造成大量函数调用,从而影响系统的性能。经过我对长函数以及短函数的大量运行测试发现:代码运行时编译器对那些短函数更加容易的进行缓存,也就是说,短函数可以更好的调动编译器的优化功能。


提取变量

变量在代码中有着各种用途,其中有些时候存在一些临时变量被多次赋值的情况,还有很多变量会用于保存一段冗长代码的运算结果。这些变量很显然在代码中不止被赋值一次,每一次赋值对于这些变量来说就是承担了一次新的责任,同一个变量承担多个责任,很显然,代码的可读性极其低下。所以,需要对这些变量进行提取。提取变量是指将一段表达式抽象出来形成一个变量。这样做的好处是可以减少代码的重复,提高代码的可读性和可维护性。下面用一个案例来说明提取变量的具体实现。

重构前的代码:

publicdoublecalculateTotalAmount(List<InvoiceItem>items) {
doubletotalAmount=0;
for (InvoiceItemitem : items) {
totalAmount+=item.getPrice() *item.getQuantity();
    }
if (totalAmount>100) {
totalAmount*=0.9;
    }
returntotalAmount;
}

可以看到,在上述代码中,计算每个项目的金额是通过 item.getPrice() * item.getQuantity() 表达式来实现的。通过提取变量的方式,对上述代码进行重构。

重构后的代码:

publicdoublecalculateTotalAmount(List<InvoiceItem>items) {
doubletotalAmount=0;
for (InvoiceItemitem : items) {
doubleitemAmount=item.getPrice() *item.getQuantity();
totalAmount+=itemAmount;
    }
if (totalAmount>100) {
totalAmount*=0.9;
    }
returntotalAmount;
}

重构后的代码将这个表达式抽象成一个变量 itemAmount,使代码更变得加易于理解和维护。


重构条件语句

写过代码的人都明白一个定理:代码的大部分功能都来自于条件判断,然而,程序的复杂度也大量来自于逻辑判断。重构的一个万年不变的话题就是条件语句的重构。条件逻辑的重构有很多方法,例如:分解条件表达式、合并条件表达式、以多态取代条件表达式等。然而他们的核心思想都是一致的:通过简化、合并或提取条件语句,使代码更加清晰和易于理解。下面用一个案例来说明重构条件语句的具体实现。

重构前的代码:

publicbooleancanCreateAccount(Customercustomer) {
booleancanCreate=true;
if (customer.getAge() <18) {
canCreate=false;
    }
if (customer.getAccountNumber() !=null&&customer.getAccountNumber().length() !=0) {
canCreate=false;
    }
if (customer.getCreditScore() <500) {
canCreate=false;
    }
returncanCreate;
}

可以看到,代码中,判断客户是否有资格创建账户的过程是通过多个条件语句实现的,如果还有其他情况,只能通过添加if/else的方法实现。显然,这不符合一段优秀代码的定义。根据重构条件语句的方式对其重构。

重构后的代码:

publicbooleancanCreateAccount(Customercustomer) {
booleancanCreate=true;
if (!isCustomerEligible(customer)) {
canCreate=false;
    }
returncanCreate;
}
privatebooleanisCustomerEligible(Customercustomer) {
if (customer.getAge() <18) {
returnfalse;
    }
if (customer.getAccountNumber() !=null&&customer.getAccountNumber().length() !=0) {
returnfalse;
    }
if (customer.getCreditScore() <500) {
returnfalse;
    }
returntrue;
}

可以看出,重构后的代码将多个条件语句合并成一个方法 isCustomerEligible(),使代码更加清晰易读。当然,对于这种多分支的逻辑语句,有各种不同的重构方法,在我的日常开发工作中,对于多条件判断的重构,首先想到的就是设计模式中的策略模式,大部分判断逻辑,都可用策略模式进行重构。本文只是列举了一种最为简单的方法,不意味着这就是最好的。


提取抽象类

提取抽象类是指将多个类中的公共方法抽象出来形成一个抽象类,使得这些类可以继承这个抽象类来继承公共方法。这样做的好处是可以减少重复代码,提高代码的复用性和可维护性。下面用一个案例来说明提取抽象类的具体实现。

重构前的代码:

publicclassSavingsAccount {
privatedoublebalance;
privatedoubleinterestRate;
publicSavingsAccount(doublebalance, doubleinterestRate) {
this.balance=balance;
this.interestRate=interestRate;
    }
publicdoublegetBalance() {
returnbalance;
    }
publicdoublegetInterestRate() {
returninterestRate;
    }
publicdoublecalculateInterest() {
returnbalance*interestRate;
    }
}
publicclassCheckingAccount {
privatedoublebalance;
privatedoubletransactionFee;
publicCheckingAccount(doublebalance, doubletransactionFee) {
this.balance=balance;
this.transactionFee=transactionFee;
    }
publicdoublegetBalance() {
returnbalance;
    }
publicdoublegetTransactionFee() {
returntransactionFee;
    }
publicdoublecalculateTransactionFee() {
returntransactionFee;
    }
}

可以看到,代码中,SavingsAccount 和 CheckingAccount 类有很多相同的方法,如 getBalance() 方法。通过提取抽象类的方式对代码进行重构。

重构后的代码:

publicabstractclassAccount {
protecteddoublebalance;
publicAccount(doublebalance) {
this.balance=balance;
    }
publicdoublegetBalance() {
returnbalance;
    }
publicabstractdoublecalculateInterest();
}
publicclassSavingsAccountextendsAccount {
privatedoubleinterestRate;
publicSavingsAccount(doublebalance, doubleinterestRate) {
super(balance);
this.interestRate=interestRate;
    }
publicdoublegetInterestRate() {
returninterestRate;
    }
publicdoublecalculateInterest() {
returnbalance*interestRate;
    }
}
publicclassCheckingAccountextendsAccount {
privatedoubletransactionFee;
publicCheckingAccount(doublebalance, doubletransactionFee) {
super(balance);
this.transactionFee=transactionFee;
    }
publicdoublegetTransactionFee() {
returntransactionFee;
    }
publicdoublecalculateTransactionFee() {
returntransactionFee;
    }
}

经过上述重构,将这些公共方法抽象成一个抽象类 Account,使得 SavingsAccount 和 CheckingAccount 类可以继承这个抽象类来继承公共方法,代码的可读性有着显著的上升。


Q&A

1.重构与设计之间的关系是什么?

我认为重构和设计可以理解成全局与局部的关系。因此重构并不是设计的改正措施,不能希望重构能把一个糟糕的设计变成优秀的设计。所以,在软件开发之前设计出优秀的系统就显得尤为重要。

2.什么时候选择重构?什么时候选择重写?

每当有功能要接入N年前的老代码的时候,重新来做一个新的系统完全替代这个老项目吧,我们可以用最新的框架,更好的实现方式去完成这个系统,这种天真的想法很多人的脑海里会无数次出现,然而,旧的系统业务很复杂,新的系统在兼容旧系统逻辑的同时,旧的系统也在更新需求,增加功能,在新系统完全可以抗衡旧系统之前,旧的系统会一直运行。如果新系统开发的时间过长,等完成的时候,可能开发者都已经不知道换了几批了,代码又乱成了一锅粥,周而复始,最后只能作罢。

至于这个问题,我相信这个问题就算是世界上最牛的架构师都无法给出一个确定性的答案。说到底,这个问题只能个人判断,很难给出一个有共通性的同类情况。不过,一些需要重写的代码肯定有迹象,例如:某个项目由于编程语言太老或者平台环境太老导致推进速度较慢,不适用于当前情况。此时,这种情况下,重构代码的作用微乎其微,就需要选择重写了。

3.如何保证重构过程中不会引入新的bug?

既然想要重构,就意味着要修改代码,修改代码就可能引入新的bug。所以,重构只能保证设计的改进,而不能保证程序没有bug。目前集团内发现bug的有效手段就是充分有效的测试,而保证充分有效的测试的关键是单元测试的高覆盖率。


结语

“重构”的核心不仅仅是一份“修代码”的指南,更为重要的是它所传达的理念:如何将一个大变化抽象为若干个细微的小变化,又在尽可能多进行细微变化的同时,不改变系统的整体表现。其实,“重构”代码并不是一项多么高深的工作,甚至很多方法显得略微基础。然而,很多人就是忽略了这些基础,才会写出大量充满着“坏味道”的代码。所以,要想减少“重构”所消耗的资源与精力,那就从基础开始写好每一行代码。

其实,写这篇文章之前,我回顾看过和写过的很多代码,让自己满意、有成就感的不多。未来路还长,以下一段代码才是我写的最好的代码的态度继续努力吧!

目录
相关文章
|
程序员
你的代码需要重构吗?
你的代码需要重构吗?
64 0
|
设计模式 算法
重构,避免重构误区
重构,避免重构误区
41 0
|
开发者
重构的理解
重构的理解
94 0
|
数据处理
《重构2》第六章-重构基础
《重构2》第六章-重构基础
306 0
|
消息中间件 缓存 负载均衡
架构重构的技巧
对软件代码做任何改动以增加可读性或者简化结构而不影响输出结果。
155 0
|
数据格式 索引
日常开发之重构
作为开发人员,应该让重构贯穿于你的开发日常的每一刻。
347 0
|
存储 设计模式 架构师
记一次项目重构
本文主要记录,刚刚步入架构师岗位4个月的我,重构项目的一些经历。
|
算法 Java 容器
狗屎一样的代码!快,重构我!
狗屎一样的代码如何重构? 重构不止是代码整理,它提供了一种高效且受控的代码整理技术。
131 0
|
消息中间件 设计模式 缓存
系统重构的道与术
准备以重构工作中容易产生误区的地方或容易被忽视的重点来聊聊,既不重复网上千篇一律的各种方案资料,也对重构工作有参考价值。
系统重构的道与术