news 2026/4/21 13:25:15

《重构:改善既有代码的设计》——以Java之名,重拾代码之美

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
《重构:改善既有代码的设计》——以Java之名,重拾代码之美

这不是一本读一遍就够的书,这是一本值得放在手边反复翻阅的编程之道。

引子:一本改变了无数程序员的书

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将多层嵌套结构扁平化,再通过filtermapfindFirst的组合,原本需要20行的复杂迭代逻辑被压缩为一行清晰的声明式管道。

手法四:使用Lombok消除样板代码——让代码回归本质

虽然《重构》原著中并未专门讨论Lombok,但在今天的Java开发中,消除样板代码是重构的一个重要维度。Lombok通过注解在编译期自动生成gettersetter、构造器、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注解自动生成了gettersettertoString()equals()hashCode()等所有常用方法。

重要提醒:Lombok虽好,但需适度使用。过度使用@Data可能因@EqualsAndHashCode包含全字段而导致性能问题或逻辑错误。团队在使用Lombok前应建立统一的规范和最佳实践。

四、重构的支撑体系:测试与工具

《重构》书中反复强调一个核心理念:没有测试,就没有重构。重构之所以能安全地进行,是因为有一套可靠的测试来验证代码行为没有被改变。在Java生态中,JUnit是测试的首选框架。每次重构前确保测试全部通过,重构后再次运行测试——如果测试通过,你就可以确信重构没有引入bug。

除了测试,现代Java IDE也为重构提供了强大的工具支持。IntelliJ IDEA把所有重构选项集中在一个快捷键Ctrl+Alt+Shift+T中,同时为常用操作设置了专属快捷键:

重构操作快捷键(Windows/Linux)快捷键(Mac)
重命名Shift + F6Shift + F6
提取方法Ctrl + Alt + MCmd + Option + M
提取变量Ctrl + Alt + VCmd + Option + V
提取常量Ctrl + Alt + CCmd + Option + C
内联Ctrl + Alt + NCmd + Option + N
移动F6F6

当进行重命名操作时,IDEA会自动更新所有相关的引用,比手动查找替换效率高很多倍。使用提取方法功能时,IDE能够自动判断变量使用范围并设计参数清单,避免了手工拆分时常见的“变量作用域问题”。

五、重构的节奏:小步快跑,持续交付

Fowler在书中提出了一条贯穿全书的核心准则:一次一小步地修改代码。每一步修改都非常简单,简单到“似乎不值得去做”,但正是这些微小步骤的累积,最终将糟糕的设计转变为良好的设计。

这条原则在Java项目中尤其适用。Java是静态类型语言,编译器的类型检查是我们的天然安全网——但前提是你不能在两次编译之间改动太多东西。

具体的重构节奏可以参考以下流程:

  1. 识别坏味道:通过代码审查、静态分析工具(SonarQube、SpotBugs)或直觉,发现需要改进的代码片段

  2. 编写/补充测试:确保目标代码有足够的测试覆盖率

  3. 选择一个重构手法:从Fowler的目录中选择最合适的手法

  4. 执行小步修改:利用IDE的重构工具,每次只做一种修改

  5. 运行测试:确保测试仍然全部通过

  6. 提交代码:每个重构步骤独立成为一个commit,便于追溯和回滚

  7. 重复:继续进行下一个重构步骤

六、重构的价值:不只是代码,更是团队的资产

《重构》一书指出,重构带来的价值远不止于“代码变漂亮了”。它可以带来以下几个维度的收益:

  • 改进软件设计:消除重复代码和坏味道,让代码结构更加清晰

  • 提高代码可读性:让后来者更容易理解和维护代码

  • 尽早发现错误:重构过程中往往能发现隐藏的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,选中一段代码,按一下快捷键,开始重构吧。你的同事和未来的自己会感谢你。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/4/21 13:22:17

终极Mac抢票指南:如何用12306ForMac轻松购买火车票

终极Mac抢票指南&#xff1a;如何用12306ForMac轻松购买火车票 【免费下载链接】12306ForMac An unofficial 12306 Client for Mac 项目地址: https://gitcode.com/gh_mirrors/12/12306ForMac 作为Mac用户&#xff0c;你是否厌倦了在春运期间与12306网页版搏斗的体验&am…

作者头像 李华
网站建设 2026/4/21 13:20:16

这个问题在开发中,如何选择适合的 API?

开发中选API&#xff0c;千万别只盯着“价格低”这三个字&#xff0c;上线后帮你踩坑的往往就是当初为了省那几块钱选的劣质接口。老手选API&#xff0c;一般都死盯以下四个维度&#xff1a;第一看&#xff1a;返回的字段&#xff0c;是不是你真正想要的 不要只看接口名字叫“查…

作者头像 李华
网站建设 2026/4/21 13:18:28

ArcGIS Pro二次开发实战:一键批量处理勘测定界TXT,自动生成GDB数据库(附编码问题解决方案)

ArcGIS Pro二次开发实战&#xff1a;勘测定界TXT自动化处理全流程解析 引言&#xff1a;勘测定界数据处理的技术痛点与解决方案 在国土空间规划、土地调查等领域&#xff0c;勘测定界数据是项目推进的基础性工作。传统作业流程中&#xff0c;技术人员常面临大量符合《勘测定界…

作者头像 李华
网站建设 2026/4/21 13:18:16

iOS开发调试终极解决方案:iOSDeviceSupport全版本支持指南

iOS开发调试终极解决方案&#xff1a;iOSDeviceSupport全版本支持指南 【免费下载链接】iOSDeviceSupport All versions of iOS Device Support 项目地址: https://gitcode.com/gh_mirrors/ios/iOSDeviceSupport iOSDeviceSupport是一款专为iOS开发者打造的设备调试兼容…

作者头像 李华