阿里云开发者社区,千万开发者的选择
阿里云开发者社区,百万精品技术内容、千节免费系统课程、丰富的体验场景、活跃的社群活动、行业专家分享交流,欢迎点击【阅读原文】加入我们。
阿里妹导读
任何一个傻瓜都可以写出计算机可以理解的代码,唯有写出人类容易理解的代码,才是优秀的程序员。 ——Martin Fowler 《重构》
讲一个小故事
前言
一、代码重构的定义
代码重构是指在不改变代码功能的前提下,通过修改代码的内部结构和外部表现形式,来提高代码的可读性、可维护性、性能和可扩展性的一种技术。代码重构通常包括以下几个方面:
二、代码重构的目的
代码重构的主要目的是提高代码的质量,使其更加易于理解、维护和扩展。具体来说,代码重构的目的包括以下几个方面:
1.提高代码的可读性
可读性是衡量代码质量的重要指标之一。鲁肃曾言:写下一行代码只要1分钟,但未来会被一代代工程师读很多次、改很多次。代码的可读性与可维护性,是我心目中好代码的第一标准。
良好的可读性可以使代码更加易于理解和维护,减少代码的错误和bug。代码重构可以通过改进代码的结构、消除代码中的冗余部分等方式来提高代码的可读性。
2.减少代码的冗余
冗余代码是指在代码中重复出现的部分。最单纯的重复代码就是“同一个类的两个函数含有相同的表达式”,冗余代码会使代码量增大,影响代码的可读性和可维护性。代码重构可以通过消除代码中的冗余部分来减少代码量,提高代码的可维护性。
3.提高代码的性能
代码重构可以通过优化代码结构和算法来提高代码的性能。具体来说,可以通过减少代码的执行时间和内存占用来提高代码的性能。虽然重构也有可能导致软件的运行速度下降,但重构之后也会使软件的性能优化更加容易,长时间看,最终的效果还是好的。
4.提高代码的可扩展性
对于一段代码的好坏,另一个重要的评价指标就是可扩展性,可扩展性是指代码在未来可以被容易地修改和扩展。代码重构可以通过改进代码结构和使用设计模式等方式来提高代码的可扩展性。一段好的代码一定是高可扩展的,这个就是代码设计方面的问题了。
三、代码重构的方法
代码重构的方法有很多种,从顶层设计到底层逻辑均可以实现重构。然而,若是所有的人力都投入到技术改造上,可能距离拥抱变化也就不远了。我们返璞归真,这里不谈多么高大上的设计方式,仅讲述笔者在开发过程中用到的几种最为常见的方法,代码较为简单,主要是体会重构的思路。
方法提取
这种重构方法是我在开发过程中最常用的一个方法,因为我经常由于一个方法过长被提了若干个CR comments。后来,团队内的一个前辈告诉我:一个方法不宜超过50行,超过50行的代码,就充斥着“代码坏味道”。
方法提取是指将一段代码抽象出来形成一个方法。这样做的好处是可以减少代码的重复,提高代码的可读性和可维护性。下面用一个案例来说明提取方法的具体实现,重构前的代码:
public void printInvoice(Invoice invoice) {
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 (InvoiceItem item : invoice.getItems()) {
System.out.println(item.getName() + " - " + item.getPrice() + " - " + item.getQuantity());
}
}
public void printInvoice(Invoice invoice) {
printInvoiceHeader(invoice);
printInvoiceItems(invoice.getItems());
}
private void printInvoiceHeader(Invoice invoice) {
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());
}
private void printInvoiceItems(List<InvoiceItem> items) {
System.out.println("Items:");
for (InvoiceItem item : items) {
System.out.println(item.getName() + " - " + item.getPrice() + " - " + item.getQuantity());
}
}
提取变量
变量在代码中有着各种用途,其中有些时候存在一些临时变量被多次赋值的情况,还有很多变量会用于保存一段冗长代码的运算结果。这些变量很显然在代码中不止被赋值一次,每一次赋值对于这些变量来说就是承担了一次新的责任,同一个变量承担多个责任,很显然,代码的可读性极其低下。所以,需要对这些变量进行提取。提取变量是指将一段表达式抽象出来形成一个变量。这样做的好处是可以减少代码的重复,提高代码的可读性和可维护性。下面用一个案例来说明提取变量的具体实现,重构前的代码:
public double calculateTotalAmount(List<InvoiceItem> items) {
double totalAmount = 0;
for (InvoiceItem item : items) {
totalAmount += item.getPrice() * item.getQuantity();
}
if (totalAmount > 100) {
totalAmount *= 0.9;
}
return totalAmount;
}
public double calculateTotalAmount(List<InvoiceItem> items) {
double totalAmount = 0;
for (InvoiceItem item : items) {
double itemAmount = item.getPrice() * item.getQuantity();
totalAmount += itemAmount;
}
if (totalAmount > 100) {
totalAmount *= 0.9;
}
return totalAmount;
}
重构条件语句
写过代码的人都明白一个定理:代码的大部分功能都来自于条件判断,然而,程序的复杂度也大量来自于逻辑判断。重构的一个万年不变的话题就是条件语句的重构。条件逻辑的重构有很多方法,例如:分解条件表达式、合并条件表达式、以多态取代条件表达式等。然而他们的核心思想都是一致的:通过简化、合并或提取条件语句,使代码更加清晰和易于理解。下面用一个案例来说明重构条件语句的具体实现,重构前的代码:
public boolean canCreateAccount(Customer customer) {
boolean canCreate = true;
if (customer.getAge() < 18) {
canCreate = false;
}
if (customer.getAccountNumber() != null && customer.getAccountNumber().length() != 0) {
canCreate = false;
}
if (customer.getCreditScore() < 500) {
canCreate = false;
}
return canCreate;
}
public boolean canCreateAccount(Customer customer) {
boolean canCreate = true;
if (!isCustomerEligible(customer)) {
canCreate = false;
}
return canCreate;
}
private boolean isCustomerEligible(Customer customer) {
if (customer.getAge() < 18) {
return false;
}
if (customer.getAccountNumber() != null && customer.getAccountNumber().length() != 0) {
return false;
}
if (customer.getCreditScore() < 500) {
return false;
}
return true;
}
提取抽象类
提取抽象类是指将多个类中的公共方法抽象出来形成一个抽象类,使得这些类可以继承这个抽象类来继承公共方法。这样做的好处是可以减少重复代码,提高代码的复用性和可维护性。下面用一个案例来说明提取抽象类的具体实现,重构前的代码:
public class SavingsAccount {
private double balance;
private double interestRate;
public SavingsAccount(double balance, double interestRate) {
this.balance = balance;
this.interestRate = interestRate;
}
public double getBalance() {
return balance;
}
public double getInterestRate() {
return interestRate;
}
public double calculateInterest() {
return balance * interestRate;
}
}
public class CheckingAccount {
private double balance;
private double transactionFee;
public CheckingAccount(double balance, double transactionFee) {
this.balance = balance;
this.transactionFee = transactionFee;
}
public double getBalance() {
return balance;
}
public double getTransactionFee() {
return transactionFee;
}
public double calculateTransactionFee() {
return transactionFee;
}
}
public abstract class Account {
protected double balance;
public Account(double balance) {
this.balance = balance;
}
public double getBalance() {
return balance;
}
public abstract double calculateInterest();
}
public class SavingsAccount extends Account {
private double interestRate;
public SavingsAccount(double balance, double interestRate) {
super(balance);
this.interestRate = interestRate;
}
public double getInterestRate() {
return interestRate;
}
public double calculateInterest() {
return balance * interestRate;
}
}
public class CheckingAccount extends Account {
private double transactionFee;
public CheckingAccount(double balance, double transactionFee) {
super(balance);
this.transactionFee = transactionFee;
}
public double getTransactionFee() {
return transactionFee;
}
public double calculateTransactionFee() {
return transactionFee;
}
}
Q&A
1.重构与设计之间的关系是什么?
我认为重构和设计可以理解成全局与局部的关系。因此重构并不是设计的改正措施,不能希望重构能把一个糟糕的设计变成优秀的设计。所以,在软件开发之前设计出优秀的系统就显得尤为重要。
2.什么时候选择重构?什么时候选择重写?
每当有功能要接入N年前的老代码的时候,重新来做一个新的系统完全替代这个老项目吧,我们可以用最新的框架,更好的实现方式去完成这个系统,这种天真的想法很多人的脑海里会无数次出现,然而,旧的系统业务很复杂,新的系统在兼容旧系统逻辑的同时,旧的系统也在更新需求,增加功能,在新系统完全可以抗衡旧系统之前,旧的系统会一直运行。如果新系统开发的时间过长,等完成的时候,可能开发者都已经不知道换了几批了,代码又乱成了一锅粥,周而复始,最后只能作罢。
至于这个问题,我相信这个问题就算是世界上最牛的架构师都无法给出一个确定性的答案。说到底,这个问题只能个人判断,很难给出一个有共通性的同类情况。不过,一些需要重写的代码肯定有迹象,例如:某个项目由于编程语言太老或者平台环境太老导致推进速度较慢,不适用于当前情况。此时,这种情况下,重构代码的作用微乎其微,就需要选择重写了。
3.如何保证重构过程中不会引入新的bug?
既然想要重构,就意味着要修改代码,修改代码就可能引入新的bug。所以,重构只能保证设计的改进,而不能保证程序没有bug。目前集团内发现bug的有效手段就是充分有效的测试,而保证充分有效的测试的关键是单元测试的高覆盖率。
结语
重构的核心不仅仅是一份“修代码”的指南,更为重要的是它所传达的理念:如何将一个大变化抽象为若干个细微的小变化,又在尽可能多进行细微变化的同时,不改变系统的整体表现。其实,“重构”代码并不是一项多么高深的工作,甚至很多方法显得略微基础。然而,很多人就是忽略了这些基础,才会写出大量充满着“坏味道”的代码。所以,要想减少重构所消耗的资源与精力,那就从基础开始写好每一行代码。
其实,写这篇文章之前,我回顾看过和写过的很多代码,让自己满意、有成就感的不多。未来路还长,以下一段代码才是我写的最好的代码的态度继续努力吧!
阿里云开发者社区,千万开发者的选择
阿里云开发者社区,百万精品技术内容、千节免费系统课程、丰富的体验场景、活跃的社群活动、行业专家分享交流,欢迎点击【阅读原文】加入我们。
文章引用微信公众号"阿里开发者",如有侵权,请联系管理员删除!