这不是一本读一遍就够的书,这是一本值得放在手边反复翻阅的编程之道。
引子:一本改变了无数程序员的书
1999年,Martin Fowler的《Refactoring: Improving the Design of Existing Code》首次面世,在软件开发领域投下了一颗重磅炸弹。二十多年来,这本书的核心理念——在不改变代码外在行为的前提下,改善其内部结构——已经成为现代软件工程不可或缺的基石。
2018年,Fowler推出了全面修订的第二版,用JavaScript重写了全部代码范例,并新增了与函数式编程相关的重构内容,以反映编程领域发生的巨大变化。但无论范例语言如何变化,书中所揭示的重构思想和原则,在不同编程语言中都有着深远的指导意义。
而对于Java开发者来说,这本书尤为特别——Java的强类型系统、丰富的IDE工具链和成熟的面向对象生态,恰好为书中每一种重构手法提供了最完美的实践土壤。
今天,就让我们以Java语言为视角,重新走进这部经典,体会重构这门艺术如何在Java的世界里生根、发芽,并最终开出优雅的代码之花。
一、什么是重构?Fowler的核心定义
Martin Fowler在书中给出了一个简洁而精确的定义:
重构(Refactoring)是这样一个过程:在不改变代码外在行为的前提下,对代码做出修改,以改进程序的内部结构。
拆解这个定义,有几个关键点值得深思:
第一,不改变外部行为。重构不是加新功能,不是修bug,不是性能优化。重构之后的代码,对调用者来说,应该和之前没有任何区别——输入相同,输出相同,行为相同。
第二,改善内部结构。重构的目标是让代码更清晰、更易读、更易维护、更易扩展。结构良好的代码就像一间整理有序的房间,当你需要找什么东西时,立刻就能找到。
第三,这是一个过程,而非一次性事件。重构不是写在项目计划书里的里程碑,而是融入日常开发的一种工作方式。
Fowler还特别强调了一个重要的方法论原则——小步迭代。每个重构步骤都十分简单,简单到“似乎不值得去做的程度”,但正是这些微小步骤的累积,才能在降低修改风险的同时,将糟糕的设计逐步转变为良好的设计。
二、为什么需要重构?从代码坏味道说起
知道“什么时候该重构”和“哪里该重构”,是一门需要培养的判断力。Fowler在书中提出了一个极具影响力的概念——代码的坏味道(Code Bad Smells)。顾名思义,坏味道不是错误,而是代码中那些“闻起来不对劲”的地方,它们暗示着更深层次的设计问题。
《重构》第一版列出了22种坏味道,第二版略有调整,但核心思路一脉相承。以下是几种在Java项目中最为常见、也最具破坏性的坏味道:
Duplicated Code(重复代码)
“如果你在一个以上的地方看到相同的程序结构,那么可以肯定:设法将它们合而为一,程序会变得更美好!”
重复代码是重构的第一靶子。当同一个计算逻辑出现在三个不同的Service类中时,每一次需求变更都意味着需要在三个地方同步修改——遗漏一处就是bug。
Long Method(过长的方法)
方法行数超出八十行,就已经散发出了明显的坏味道。长方法往往同时做了多件事,违反了“单一职责原则”,也让调用者难以快速理解这个方法到底在做什么。
Large Class(过大的类)
一个类承载了太多职责,既有订单计算逻辑,又有邮件发送功能,还有权限校验——这是典型的“上帝类”问题。
Switch Statements(switch惊悚现身)
散落在代码各处的switch和长串if-else,往往是多态用得不充分的信号。每当新增一种类型,就需要找到所有switch语句逐一添加分支,极易遗漏。
Primitive Obsession(基本类型偏执)
用String表示用户ID、用int表示订单状态、用List<String>存储地址信息……基本类型偏执让代码失去了类型安全和语义表达力。
Feature Envy(依恋情结)
当一个方法过度依赖于另一个类的数据和方法,而对自己所在类的特性视而不见时,就犯了“依恋情结”。这通常意味着职责放错了地方。
给Java开发者的提示:代码坏味道不是错误,而是重构的契机。闻到味道不一定要立刻行动,但至少要知道“这里可能需要改进”。
三、重构手法精粹:Java视角下的经典案例
《重构》一书中收录了70余种具体的重构手法,涵盖函数重组、数据整理、条件表达式简化等多个模块。以下选取几个在Java项目中最常用、也最能体现Java语言特性的手法,逐一剖析。
手法一:提炼函数(Extract Method)——Java重构的起点
动机:如果一个方法太长,或者代码中的一段注释提示了“这里应该做什么”,那就应该把这段代码提炼成一个独立的方法。
重构前:
public void printInvoice() { // 打印抬头 System.out.println("Invoice ID: " + id); System.out.println("Customer: " + customerName); System.out.println("Date: " + date); // 计算并打印明细 double total = 0; for (Item item : items) { total += item.getPrice() * item.getQuantity(); System.out.println(item.getName() + ": " + item.getPrice() + " x " + item.getQuantity()); } // 计算并打印总计 System.out.println("Subtotal: " + total); double tax = total * 0.08; System.out.println("Tax (8%): " + tax); double finalTotal = total + tax; System.out.println("Total: " + finalTotal); }重构后:
public void printInvoice() { printHeader(); double total = calculateSubtotal(); printItems(); printTotals(total); } private void printHeader() { System.out.println("Invoice ID: " + id); System.out.println("Customer: " + customerName); System.out.println("Date: " + date); } private double calculateSubtotal() { return items.stream() .mapToDouble(item -> item.getPrice() * item.getQuantity()) .sum(); } private void printItems() { items.forEach(item -> System.out.println(item.getName() + ": " + item.getPrice() + " x " + item.getQuantity()) ); } private void printTotals(double subtotal) { System.out.println("Subtotal: " + subtotal); double tax = subtotal * 0.08; System.out.println("Tax (8%): " + tax); System.out.println("Total: " + (subtotal + tax)); }Java开发者的注意事项:在Java中提炼方法时,要特别注意变量作用域。如果被提炼的代码块中使用了局部变量,IDE的重构工具(如IntelliJ IDEA)会自动帮你判断哪些变量需要作为参数传入,哪些需要作为返回值返回。熟练掌握快捷键Ctrl+Alt+M(Windows/Linux)或Cmd+Option+M(Mac),能极大提升重构效率。
手法二:以多态取代条件表达式——面向对象重构的精髓
这是《重构》一书中最能体现面向对象思想的手法之一。
动机:如果你需要根据对象的类型执行不同的行为,那么更好的做法是将行为封装到类型对应的子类中,利用多态来消除条件分支。
重构前:
public class Bird { private String type; private int numberOfCoconuts; private double voltage; private boolean isNailed; public double getSpeed() { if (type.equals("European")) { return getBaseSpeed(); } else if (type.equals("African")) { return getBaseSpeed() - getLoadFactor() * numberOfCoconuts; } else if (type.equals("NorwegianBlue")) { return isNailed ? 0 : getBaseSpeed(voltage); } throw new RuntimeException("Unknown bird type"); } private double getBaseSpeed() { return 10; } private double getLoadFactor() { return 2; } private double getBaseSpeed(double voltage) { return voltage * 5; } }重构后:
// 抽象基类 public abstract class Bird { public abstract double getSpeed(); } // 欧洲鸟 public class European extends Bird { @Override public double getSpeed() { return getBaseSpeed(); } private double getBaseSpeed() { return 10; } } // 非洲鸟 public class African extends Bird { private int numberOfCoconuts; public African(int numberOfCoconuts) { this.numberOfCoconuts = numberOfCoconuts; } @Override public double getSpeed() { return getBaseSpeed() - getLoadFactor() * numberOfCoconuts; } private double getBaseSpeed() { return 10; } private double getLoadFactor() { return 2; } } // 挪威蓝鹦鹉 public class NorwegianBlue extends Bird { private double voltage; private boolean isNailed; public NorwegianBlue(double voltage, boolean isNailed) { this.voltage = voltage; this.isNailed = isNailed; } @Override public double getSpeed() { return isNailed ? 0 : getBaseSpeed(voltage); } private double getBaseSpeed(double voltage) { return voltage * 5; } }深度思考:重构后的代码遵循了开闭原则——对扩展开放,对修改封闭。新增一种鸟类时,只需添加一个新的子类,完全不需要修改任何现有代码。这正是面向对象设计的精髓所在。
在实际项目中,你可能会遇到更复杂的场景:类型判断散落在多处,或者类型本身是动态变化的。此时,可以结合策略模式(Strategy Pattern)或状态模式(State Pattern)来替代简单的继承多态。
手法三:以Optional取代null检查——拥抱Java 8的现代特性
《重构》第二版特别新增了与函数式编程相关的内容,这对Java 8及更高版本的开发者来说尤为实用。
动机:Java中最常见的运行时错误是什么?NullPointerException。大量嵌套的if (obj != null)检查让代码变得臃肿且难以阅读。Java 8引入的Optional类型提供了一种更优雅的处理方式。
重构前:
public String getCityName(Address address) { if (address != null) { if (address.getCity() != null) { if (address.getCity().getName() != null) { return address.getCity().getName(); } } } return "Unknown"; }重构后:
public String getCityName(Address address) { return Optional.ofNullable(address) .map(Address::getCity) .map(City::getName) .orElse("Unknown"); }更进一步——结合Stream API:
// 重构前:在集合中查找符合条件的第一个元素,冗长且易错 public Optional<MediaName> getFirstMediaName(List<Participant> participants) { for (Participant p : participants) { for (ParticipantDevice device : p.getDevices()) { if (device.getMedia() != null && device.getMedia().getMediaType() != null) { String mediaType = device.getMedia().getMediaType().toUpperCase(); if (mediaToNameMap.containsKey(mediaType)) { return Optional.of(mediaToNameMap.get(mediaType)); } } } } return Optional.empty(); } // 重构后:Stream API的声明式管道 public Optional<MediaName> getFirstMediaName(List<Participant> participants) { Map<String, String> mediaToNameMap = Config.getMediaMap(); return participants.stream() .flatMap(p -> p.getDevices().stream()) .map(ParticipantDevice::getMedia) .filter(Objects::nonNull) .map(Media::getMediaType) .filter(StringUtils::isNotBlank) .map(String::toUpperCase) .filter(mediaToNameMap::containsKey) .map(mediaToNameMap::get) .findFirst(); }通过flatMap将多层嵌套结构扁平化,再通过filter、map和findFirst的组合,原本需要20行的复杂迭代逻辑被压缩为一行清晰的声明式管道。
手法四:使用Lombok消除样板代码——让代码回归本质
虽然《重构》原著中并未专门讨论Lombok,但在今天的Java开发中,消除样板代码是重构的一个重要维度。Lombok通过注解在编译期自动生成getter、setter、构造器、toString等代码,让开发者从这些机械重复的工作中解放出来。
重构前(手动编写的DTO):
public class UserDTO { private Long id; private String username; private String email; private String phone; public UserDTO() {} public UserDTO(Long id, String username, String email, String phone) { this.id = id; this.username = username; this.email = email; this.phone = phone; } public Long getId() { return id; } public void setId(Long id) { this.id = id; } public String getUsername() { return username; } public void setUsername(String username) { this.username = username; } public String getEmail() { return email; } public void setEmail(String email) { this.email = email; } public String getPhone() { return phone; } public void setPhone(String phone) { this.phone = phone; } @Override public String toString() { return "UserDTO{id=" + id + ", username='" + username + "'...}"; } }重构后(使用Lombok):
import lombok.Data; import lombok.NoArgsConstructor; import lombok.AllArgsConstructor; @Data @NoArgsConstructor @AllArgsConstructor public class UserDTO { private Long id; private String username; private String email; private String phone; }代码量从约50行骤降至10行左右,且@Data注解自动生成了getter、setter、toString()、equals()和hashCode()等所有常用方法。
重要提醒:Lombok虽好,但需适度使用。过度使用
@Data可能因@EqualsAndHashCode包含全字段而导致性能问题或逻辑错误。团队在使用Lombok前应建立统一的规范和最佳实践。
四、重构的支撑体系:测试与工具
《重构》书中反复强调一个核心理念:没有测试,就没有重构。重构之所以能安全地进行,是因为有一套可靠的测试来验证代码行为没有被改变。在Java生态中,JUnit是测试的首选框架。每次重构前确保测试全部通过,重构后再次运行测试——如果测试通过,你就可以确信重构没有引入bug。
除了测试,现代Java IDE也为重构提供了强大的工具支持。IntelliJ IDEA把所有重构选项集中在一个快捷键Ctrl+Alt+Shift+T中,同时为常用操作设置了专属快捷键:
| 重构操作 | 快捷键(Windows/Linux) | 快捷键(Mac) |
|---|---|---|
| 重命名 | Shift + F6 | Shift + F6 |
| 提取方法 | Ctrl + Alt + M | Cmd + Option + M |
| 提取变量 | Ctrl + Alt + V | Cmd + Option + V |
| 提取常量 | Ctrl + Alt + C | Cmd + Option + C |
| 内联 | Ctrl + Alt + N | Cmd + Option + N |
| 移动 | F6 | F6 |
当进行重命名操作时,IDEA会自动更新所有相关的引用,比手动查找替换效率高很多倍。使用提取方法功能时,IDE能够自动判断变量使用范围并设计参数清单,避免了手工拆分时常见的“变量作用域问题”。
五、重构的节奏:小步快跑,持续交付
Fowler在书中提出了一条贯穿全书的核心准则:一次一小步地修改代码。每一步修改都非常简单,简单到“似乎不值得去做”,但正是这些微小步骤的累积,最终将糟糕的设计转变为良好的设计。
这条原则在Java项目中尤其适用。Java是静态类型语言,编译器的类型检查是我们的天然安全网——但前提是你不能在两次编译之间改动太多东西。
具体的重构节奏可以参考以下流程:
识别坏味道:通过代码审查、静态分析工具(SonarQube、SpotBugs)或直觉,发现需要改进的代码片段
编写/补充测试:确保目标代码有足够的测试覆盖率
选择一个重构手法:从Fowler的目录中选择最合适的手法
执行小步修改:利用IDE的重构工具,每次只做一种修改
运行测试:确保测试仍然全部通过
提交代码:每个重构步骤独立成为一个commit,便于追溯和回滚
重复:继续进行下一个重构步骤
六、重构的价值:不只是代码,更是团队的资产
《重构》一书指出,重构带来的价值远不止于“代码变漂亮了”。它可以带来以下几个维度的收益:
改进软件设计:消除重复代码和坏味道,让代码结构更加清晰
提高代码可读性:让后来者更容易理解和维护代码
尽早发现错误:重构过程中往往能发现隐藏的bug
提高编程速度:良好的设计让后续开发更顺畅,后发优势不可低估
对于团队协作而言,重构还意味着降低沟通成本——当代码清晰到“自解释”的程度时,代码审查的效率会大幅提升,新成员的上手速度也会加快。
七、重构的边界:知道何时停止
重构是一门艺术,而非科学。Fowler在书中并没有给出“何时停止重构”的硬性规则,但有一些经验性的边界值得参考:
不要为了重构而重构:如果代码运行良好且不会再被修改,重构的价值就非常有限
不要在发布前大规模重构:重构应当在开发周期中持续进行,而非集中在发布前夕
不要过度设计:为今天的需求重构,但不要为明天可能永远不会出现的需求过度设计
保持团队共识:重构不是个人行为,应当有团队层面的共识和规范
结语:重构是一种日常习惯,Java是最好的战场
Martin Fowler在书中引用了一句广为流传的名言:
“Any fool can write code that a computer can understand. Good programmers write code that humans can understand.”
能够写出计算机能够理解的代码,是每个程序员的基本功。而写出人类能够理解的代码,才是优秀程序员的标志。
重构不是一次性的活动,而是融入日常开发的一种习惯——每次修改代码时,顺便改进一下附近的结构。日积月累,代码库会保持健康,技术债务不会失控。
而Java语言,凭借其强类型系统带来的安全保障、成熟的IDE工具链提供的重构能力,以及丰富的面向对象和函数式特性,为这门重构的艺术提供了最理想的实践土壤。
如果说《重构:改善既有代码的设计》是一本关于“如何写好代码”的哲学书,那么Java就是实践这些哲学思想的绝佳舞台。希望每一位Java开发者都能从中获得启发,让手中的代码更优雅、更健壮、更经得起时间的考验。
现在,打开你的IDE,选中一段代码,按一下快捷键,开始重构吧。你的同事和未来的自己会感谢你。