背景
在软件开发的世界里,代码重构是提升项目质量、适应业务变化的关键步骤。最近,我重新翻阅了《重构:改善既有代码的设计 第二版》,这本书不仅重新点燃了我对重构的热情,还深化了我的理解:重构不仅仅是代码层面的整理,它更是一种软件开发的哲学,强调持续改进和适应变化的重要性。
书中通过详细的案例分析和代码示例,将理论与实践巧妙地融合在一起。我尤其赞赏作者如何将复杂的重构任务拆解成一系列的小步骤,每一步都被精心设计和考虑,大大降低了重构过程中的风险,同时提高了整个过程的可控性。
在这篇文章中,我将通过《重构:改善既有代码的设计 第二版》书中知识、以及结合在过去几年中的重构经历(大到系统架构、核心接口、底层数据存储、小到简单的一个方法),分享一些关于重构的感悟和心得。通过有真实的场景、实际重构案例的剖析,我们可以更深刻地理解重构不仅是代码层面的改进,更是一种思维方式,指引我们如何在不断变化的业务需求面前,持续优化和提升软件的质量与效能。
背景
在软件开发的世界里,代码重构是提升项目质量、适应业务变化的关键步骤。最近,我重新翻阅了《重构:改善既有代码的设计 第二版》,这本书不仅重新点燃了我对重构的热情,还深化了我的理解:重构不仅仅是代码层面的整理,它更是一种软件开发的哲学,强调持续改进和适应变化的重要性。
书中通过详细的案例分析和代码示例,将理论与实践巧妙地融合在一起。我尤其赞赏作者如何将复杂的重构任务拆解成一系列的小步骤,每一步都被精心设计和考虑,大大降低了重构过程中的风险,同时提高了整个过程的可控性。
重构的定义与理念
正确定义问题,比解决问题重要一百倍。那我们首先来搞清楚什么叫重构?
作为(动词),重构意味着通过一系列细微的步骤,不断地调整软件结构,以保持其设计的整洁和可维护性。
重构是一种精练的技艺,它通过小的、计划好的修改来减少引入错误的风险。本质上,重构是对已完成的代码进行设计上的改进。
02
重构的边界与时机
1)重构边界
在软件架构中,API(应用程序编程接口)和数据库(DB)的设计至关重要,因为它们分别代表了系统的外部交互界面和内部数据存储机制。良好的设计不仅能够提高系统的稳定性、可扩展性和可维护性,而且在未来进行代码重构或系统升级时,也能大大减少对上游服务和数据迁移的影响。
1.1)API设计的重要性
1.抽象层次:API作为系统与外界交互的接口,提供了一层抽象,隐藏了底层的业务逻辑和实现细节。这意味着,只要API的接口保持不变,系统内部的实现可以自由变化而不影响外部调用者。
1.2)数据库设计的重要性
1.扩展性和可维护性:随着系统的发展,数据量会增加,业务需求也会变化。一个设计良好的数据库能够更容易地进行扩展和维护,比如通过合理的索引设计、分表分库等策略来提高性能。
1.3)代码重构的考虑
分离关注点:即使内部代码结构复杂或混乱,通过良好设计的API和数据库,也可以将内部重构的影响限制在系统内部,避免波及到外部调用者或导致数据丢失、不一致等问题。
迭代开发:在保持API接口稳定和数据库设计前瞻性的前提下,可以更自由地对内部代码进行迭代开发和重构,逐步改进系统的内部质量而不影响外部使用者。
上游和下游的协调:良好的API和数据库设计,可以减少在系统升级或重构时对上游服务的影响和对数据库的数据迁移需求。这意味着,即使需要进行较大的内部修改,也能保障系统的整体稳定性和数据的一致性。
2)为什么要重构
3)什么时候重构
3.1)线上痛点&风险可控
3.2)预备性重构:新需求功能更容易
3.3)预备性重构:数据优化减负
4)什么时候不需要重构
重构的实践与步骤
1)清晰的重构目标
2)逐步重构
3)测试和比对
4)切量验证
5)重构后评估
通过遵循这些评估步骤,重构可以以一种有序和系统化的方式进行,这不仅最小化了引入新问题的风险,还有助于提升软件的整体质量。最终,这将使得软件更加健壮、易于维护,并且能够更好地适应未来的变化和需求。
重构的挑战
1)重构的成本
1.1)时间和资源消耗
1.2)延缓新功能开发
2)重构的风险
2.1)引入新的错误
重构小技巧
书中通过具体的代码示例展示了如何执行重构,并解释了每种重构的动机、做法和效果。以下是一些重要的重构技术和案例:
1)提炼函数(Extract Function)
重构前:
public class AccountService {
public void createAccount(String email, String username, String pwd) {
if (email == null || email.isEmpty()) {
throw new IllegalArgumentException("Email cannot be empty.");
}
if (username == null || username.isEmpty()) {
throw new IllegalArgumentException("Username cannot be empty.");
}
if (pwd == null || pwd.isEmpty()) {
throw new IllegalArgumentException("pwd cannot be empty.");
}
// 在这里插入数据库操作代码,创建账户
// 发送欢迎邮件
String welcomeMessage="Dear " + username + ", welcome to our service!";
// 在这里插入邮件发送代码
}
}
在这段代码中,createAccount
方法同时负责验证输入、创建账户和发送邮件。我们可以通过提炼函数来拆分这个方法。
重构后:
public class AccountService {
public void createAccount(String email, String username, String pwd) {
validateAccountDetails(email, username, pwd);
insertAccountIntoDatabase(email, username, pwd);
sendWelcomeEmail(username);
}
private void validateAccountDetails(String email, String username, String pwd) {
if (email == null || email.isEmpty()) {
throw new IllegalArgumentException("Email cannot be empty.");
}
if (username == null || username.isEmpty()) {
throw new IllegalArgumentException("Username cannot be empty.");
}
if (pwd == null || pwd.isEmpty()) {
throw new IllegalArgumentException("pwd cannot be empty.");
}
}
private void insertAccountIntoDatabase(String email, String username, String password) {
// 在这里插入数据库操作代码,创建账户
}
private void sendWelcomeEmail(String username) {
String welcomeMessage="Dear " + username + ", welcome to our service!";
// 在这里插入邮件发送代码
}
}
createAccount
方法中的三个主要任务分别提炼到了三个独立的私有方法中。每个方法都有明确的职责:验证账户信息、插入账户到数据库和发送欢迎邮件。这样的代码更加清晰,每个部分都更容易理解和测试。此外,如果将来我们需要在其他地方验证账户信息或发送邮件,我们可以复用这些已经提炼出来的方法,增加了代码的可重用性。2)内联函数(Inline Function)
内联函数(Inline Function)是一种重构技术,用于将一个函数的内容移动到该函数被调用的地方,然后移除原函数。这种技术通常用于当一个函数的体积非常小,而且只被使用一次或者函数的内容几乎和它的名字一样清晰时。
下面我们通过一个例子来说明内联函数的重构过程。
重构前的代码
LogisticsService
类,它有一个calculateShippingCost
方法,这个方法只是简单地调用了另一个方法getBaseShippingCost
。如果getBaseShippingCost
方法只在这里被调用,我们就可以考虑使用内联函数。public class LogisticsService {
public double processOrder(Order order) {
// 其他处理逻辑...
doubleshippingCost= calculateShippingCost(order);
// 其他处理逻辑...
return shippingCost;
}
private double calculateShippingCost(Order order) {
return getBaseShippingCost(order);
}
private double getBaseShippingCost(Order order) {
doublebaseCost=0.0;
return baseCost;
}
}
重构后的代码
现在我们将calculateShippingCost
方法内联到processOrder
方法中,并移除calculateShippingCost
方法。
public class LogisticsService {
public double processOrder(Order order) {
// 其他处理逻辑...
double shippingCost= getBaseShippingCost(order);
// 其他处理逻辑...
return shippingCost;
}
private double getBaseShippingCost(Order order) {
double baseCost=0.0;
return baseCost;
}
}
在这个重构后的例子中,我们直接在processOrder
方法中调用getBaseShippingCost
来计算运费,从而去除了多余的calculateShippingCost
方法。这样做简化了代码结构,减少了一层不必要的抽象,使得代码更加直接和清晰。
3)提炼变量(Extract Variable)
将表达式的结果赋给一个临时变量,以提高表达式的清晰度。
重构前:
if (order.getTotalPrice() - order.getDiscounts() > 100) {
// 逻辑处理
}
重构后:
doublenetPrice= order.getTotalPrice() - order.getDiscounts();
if (netPrice > 100) {
// 逻辑处理
}
4)内联变量(Inline Variable)
如果一个临时变量只被赋值一次,然后被直接使用,可以将其替换为直接使用赋值表达式。
重构前:
doublebasePrice= order.basePrice();
return (basePrice > 1000);
return order.basePrice() > 1000;
5)引入参数对象(Introduce Parameter Object)
将多个函数参数替换为一个对象,当多个函数共享几个参数时尤其有用,常用context上下文数据传递
重构前:
public void trest(String logPrefix, A a,B b,C c) {
//业务逻辑处理
}
public void trest(Context context) {
//业务逻辑处理
}
6)分解条件表达式(Decompose Conditional)
将复杂的条件逻辑分解为更清晰的逻辑块,提高其可读性。
重构前:
public void applyFee(Account account) {
if (account.getBalance() < 0 && account.isOverdraftEnabled()) {
account.addFee(OVERDRAFT_FEE);
}
}
public void applyFee(Account account) {
if (shouldApplyOverdraftFee(account)) {
account.addFee(OVERDRAFT_FEE);
}
}
private boolean shouldApplyOverdraftFee(Account account) {
return account.getBalance() < 0 && account.isOverdraftEnabled();
}
7)合并条件表达式(Consolidate Conditional Expression)
将多个条件表达式合并为一个,简化逻辑判断。
重构前:
if (isSpecialDeal()) {
total = price * 0.95;
} else {
total = price * 0.98;
}
total = price * (isSpecialDeal() ? 0.95 : 0.98);
8)移除死代码(Remove Dead Code)
删除不再被使用的代码,减少维护负担。
重构切量验证完成后,确保老代码无用,可直接删除主赠老逻辑calcTransferTimeForGift方法以及下面依赖的方法(前提是这些方法没有其他地方依赖使用)。
《重构:改善既有代码的设计 第二版》是一本值得每位专业程序员阅读的指南。这本书深入探讨了重构的概念、过程、技术和案例,旨在指导开发者如何通过一系列小的、控制风险的代码修改来逐步改进代码的内部结构,而不改变其外部行为。这不仅提升了软件的业务价值和灵活性,也使我们成为了能够写出人类易于理解代码的优秀程序员。
如果您有任何其他关于重构的建议或想法,欢迎评论交流,谢谢!