如何识别和解决 Java 代码中的坏味道

简介: 编程中,代码质量随着时间推移逐渐退化是一个普遍问题,这种现象被称为代码坏味道(Code Smell)。代码坏味道并不意味着代码有错误,而是指出可能存在更深层问题的迹象,影响代码的可读性、可维护性和扩展性。识别和解决代码坏味道是提升代码质量的关键步骤。

作为程序员,大家都知道在软件研发的过程中,代码质量的退化是一个常见的问题,也是一个必然的现象,这种现象称之为代码坏味道,它指的是一些可能指示着更深层次问题的迹象。


坏味道本身并不代表存在错误,但是通常是代码维护困难和扩展性差的征兆。识别和解决这些坏味道是我们提升代码质量的重要步骤。


今天灸哥和大家一起聊聊我们常见的代码坏味道以及解决之道。


识别坏味道


代码坏味道的识别一般是要求开发者具备一定的代码审查能力和对设计原则相关的理解,同时也需要一定的经验和技巧,在日常编码过程中,以下三个手段是可以有助于你识别和解决坏味道:


  1. 代码审查:定期组织团队成员对代码进行审查,可以借助集体智慧来发现潜在的坏味道并及时修复
  2. 代码分析:使用静态的代码分析工具可以帮助程序员自动识别一些常见的代码坏味道
  3. 重构实践:通过不断地重构代码,可以逐渐消除坏味道,提高代码质量


常见坏味道


过长方法


过长方法的坏味道一般表现为方法过于冗长,包含多个逻辑分支和多个职责,难以理解和维护。


具体的表现为:

  • 方法行数超过 50 或者 100 行
  • 方法中有多个嵌套的 if/else 语句
  • 方法的命名难以表达其所有逻辑


一般针对过长方法的解决路径如下:

  • 将方法分解为更小的、职责单一的函数
  • 移除重复代码,使用辅助方法来提高可读性
  • 简化条件表达式,使用早返回减少嵌套


我们来看看具体的代码示例

// 坏味道代码
public void processOrder(Order order) {
    if (order.isValid()) {
        if (order.isExpress()) {
            calculateExpressShipping(order);
        } else {
            calculateStandardShipping(order);
        }
        if (order.isDiscountEligible()) {
            applyDiscount(order);
        }
        persistOrder(order);
    } else {
        throw new IllegalArgumentException("Invalid order");
    }
}
// 重构后的代码
private void validateOrder(Order order) {
    if (!order.isValid()) {
        throw new IllegalArgumentException("Invalid order");
    }
}
private void calculateShipping(Order order) {
    if (order.isExpress()) {
        calculateExpressShipping(order);
    } else {
        calculateStandardShipping(order);
    }
}
private void applyDiscountIfEligible(Order order) {
    if (order.isDiscountEligible()) {
        applyDiscount(order);
    }
}
private void persistOrder(Order order) {
    // Persist order logic...
}


重复代码


重复代码的坏味道一般表现为相同或者非常相似的代码片段在不同的地方重复出现。


具体的表现为:

  • 多个方法或类中有几乎相同的代码块
  • 复制粘贴修改的研发模式


一般针对过长方法的解决路径如下:

  • 将重复的代码提取到一个公共的方法或者类中
  • 使用继承或者组合来共享代码


我们来看看具体的代码示例

// 坏味道代码
public int calculateTotalForOrderA(Order order) {
    int total = 0;
    for (Item item : order.getItems()) {
        total += item.getPrice();
    }
    return total * 0.9; // 10% discount
}
public int calculateTotalForOrderB(Order order) {
    int total = 0;
    for (Item item : order.getItems()) {
        total += item.getPrice();
    }
    return total * 0.9; // 10% discount
}
// 重构后的代码
public int calculateTotalWithDiscount(Order order) {
    int total = 0;
    for (Item item : order.getItems()) {
        total += item.getPrice();
    }
    return total * 0.9; // 10% discount
}


过大的类


过大的类的坏味道一般表现为类承担了过多的职责,包含多个不相关的功能。


具体的表现为:

  • 类中有多个互不相关的功能方法
  • 类的职责难以通过类名表达


一般针对过长方法的解决路径如下:

  • 将类分解为多个更小的类,每个类负责单一职责
  • 使用继承或者接口来组织相关功能


我们来看看具体的代码示例

// 坏味道代码
class User {
    // 用户认证相关方法...
    // 用户信息管理相关方法...
    // 用户订单处理相关方法...
}
// 重构后的代码
class Authenticator {
    // 用户认证相关方法...
}
class UserProfile {
    // 用户信息管理相关方法...
}
class OrderManager {
    // 用户订单处理相关方法...
}


全局状态


全局状态的坏味道一般表现为系统的多个部分依赖于全局变量或者单例状态,导致难以追踪和维护。


具体的表现为:

  • 多个类依赖于同一个全局变量或者单例对象
  • 状态的变化影响整个系统的行为


一般针对过长方法的解决路径如下:

  • 将全局状态封装到类中,提供方法来访问和修改状态
  • 使用依赖注入来管理依赖关系


我们来看看具体的代码示例

// 坏味道代码
class Config {
    private static int maxUsers = 100;
    public static int getMaxUsers() {
        return maxUsers;
    }
    public static void setMaxUsers(int maxUsers) {
        Config.maxUsers = maxUsers;
    }
}
// 重构后的代码
class AppConfig {
    private int maxUsers = 100;
    public int getMaxUsers() {
        return maxUsers;
    }
    public void setMaxUsers(int maxUsers) {
        this.maxUsers = maxUsers;
    }
}


魔法数字


魔法数字的坏味道一般表现为代码中直接使用了未定义的数字常量,缺乏可读性。


具体的表现为:

  • 数字值在代码中多次出现,但没有明确的含义
  • 数字与代码逻辑紧密相关,但未通过命名常量表示


一般针对过长方法的解决路径如下:

  • 将魔法数字替换为命名常量或者配置项
  • 使用美剧或者类常量来提供更好的可读性


我们来看看具体的代码示例

// 坏味道代码
if (list.size() > 10) {
    // ...
}
// 重构后的代码
public static final int MAX_SIZE = 10;
if (list.size() > MAX_SIZE) {
    // ...
}


神秘代码


神秘代码的坏味道一般表现为代码中存在难以理解的复杂表达式或者算法,缺乏注释或者文档说明。


具体的表现为:

  • 代码逻辑复杂,难以一眼看出其意图
  • 缺少文档或者注释,其他开发者难以快速理解代码


一般针对过长方法的解决路径如下:

  • 简化复杂表达式,使用辅助方法或者函数
  • 补充必要的文档或者注释,清晰说明代码的目的或者逻辑


我们来看看具体的代码示例

// 坏味道代码
int result = (a * 3) + (b / 2) - 5;
// 重构后的代码
private static final int MULTIPLIER_A = 3;
private static final int DIVIDER_B = 2;
private static final int SUBTRACTION_CONSTANT = 5;
int result = (a * MULTIPLIER_A) + (b / DIVIDER_B) - SUBTRACTION_CONSTANT;


数据泥团


数据泥团的坏味道一般表现为多个数据项经常一起使用,但是没有封装在一起,导致数据管理混乱。


具体的表现为:

  • 多个变量经常一起出现,但是未作为一个整体处理
  • 数据项之间的关联关系未在代码中体现


一般针对过长方法的解决路径如下:

  • 创建一个新的类或者数据结构来封装这些数据项
  • 使用对象或者集合来管理这些数据项的关系


我们来看看具体的代码示例

// 坏味道代码
int customerId;
String customerName;
Date customerSince;
// 重构后的代码
class Customer {
    private int id;
    private String name;
    private Date since;
    // 构造函数、getter和setter...
}


过度耦合


过度耦合的坏味道一般表现为类之间或者模块之间的依赖关系过于紧密,一个变更可能会影响多个部分。


具体的表现为:

  • 一个类的改变需要修改多个其他类
  • 类或者模块之间的接口过于复杂


一般针对过长方法的解决路径如下:

  • 减少类之间的直接依赖,使用接口或者抽象类来解耦
  • 采用设计模式,比如观察者模式、策略模式等,来降低耦合度


我们来看看具体的代码示例

// 坏味道代码
class ReportGenerator {
    private Printer printer;
    private Database database;
    public void generateReport() {
        // ...
        printer.print(report);
        // ...
        List<Data> data = database.getData();
        // ...
    }
}
// 重构后的代码
class ReportGenerator {
    private ReportPrinter reportPrinter;
    private ReportDataAccessor dataAccessor;
    public void generateReport() {
        List<Data> data = dataAccessor.getData();
        Report report = createReport(data);
        reportPrinter.printReport(report);
    }
}
interface ReportPrinter {
    void printReport(Report report);
}
interface ReportDataAccessor {
    List<Data> getData();
}


过复杂条件


过复杂条件的坏味道一般表现为条件语句过于复杂,难以理解和维护。


具体的表现为:

  • 多层嵌套的 if/else 语句
  • 复杂的逻辑表达式,难以一眼看出其逻辑


一般针对过长方法的解决路径如下:

  • 使用多态、策略模式或者状态模式来简化条件判断
  • 将复杂条件分解为多个简单的条件,使用卫语句


我们来看看具体的代码示例

// 坏味道代码
if (user.isAdmin() && date.isWeekend() && item.isInStock()) {
    // ...
}
// 重构后的代码
if (!user.canMakePurchase()) {
    return;
}
if (!item.isAvailable()) {
    return;
}
if (!isPurchaseTimeValid(date)) {
    return;
}
// ...


发散式变化


发散式变化的坏味道一般表现为修改一处代码需要在多个地方进行更新,导致维护困难。


具体的表现为:

  • 应用一处变更时,需要修改多个文件或者类
  • 类或者模块的变更频繁,且互相影响


一般针对过长方法的解决路径如下:

  • 重构代码,减少类或者模块之间的耦合
  • 引入新的抽象层或者使用组合代替继承


我们来看看具体的代码示例

// 坏味道代码
class Product {
    private String name;
    private double price;
    public void updatePrice(double newPrice) {
        this.price = newPrice;
        notifyPriceChange();
    }
    private void notifyPriceChange() {
        // Notify multiple systems...
    }
}
// 重构后的代码
class Product {
    private String name;
    private double price;
    private List<PriceChangeListener> listeners;
    public void updatePrice(double newPrice) {
        this.price = newPrice;
        notifyListeners();
    }
    private void notifyListeners() {
        for (PriceChangeListener listener : listeners) {
            listener.priceChanged(this);
        }
    }
    public void addPriceChangeListener(PriceChangeListener listener) {
        listeners.add(listener);
    }
}
interface PriceChangeListener {
    void priceChanged(Product product);
}


特征羡慕


特征羡慕的坏味道一般表现为一个类频繁使用另一个类的方法或者属性,显示出对这个类的过度依赖。


具体的表现为:

  • 一个类的方法主要操作另一个类的属性
  • 一个类包含多个与另一个类紧密相关的功能


一般针对过长方法的解决路径如下:

  • 重新组织类的结构,将羡慕的类或者属性移动到依赖它的类中
  • 建立新的类来封装羡慕的特征


我们来看看具体的代码示例

// 坏味道代码
class OrderService {
    private Order order;
    private Customer customer;
    public void processOrder() {
        // ...
        order.setShippingAddress(customer.getShippingAddress());
        // ...
    }
}
// 重构后的代码
class CustomerService {
    private Customer customer;
    public void updateShippingAddress(String address) {
        customer.setShippingAddress(address);
    }
}
class Order {
    private Customer customer;
    // ...
}


我本次列举出比较常见的代码坏味道,除了这些还有其他的代码坏味道,欢迎留言交流,也欢迎大家继续总结关于代码坏味道的内容。


通用识别和解决这些常见的代码坏味道,是可以显著提高代码的质量和可维护性的。重构不仅仅是代码改进的过程,也是开发者技能提升的过程。通过持续的实践和学习,我们可以更好地写出清晰、健壮和易于维护的代码。

相关文章
|
4天前
|
Java
在 Java 中捕获和处理自定义异常的代码示例
本文提供了一个 Java 代码示例,展示了如何捕获和处理自定义异常。通过创建自定义异常类并使用 try-catch 语句,可以更灵活地处理程序中的错误情况。
|
26天前
|
存储 安全 Java
Java Map新玩法:探索HashMap和TreeMap的高级特性,让你的代码更强大!
【10月更文挑战第17天】Java Map新玩法:探索HashMap和TreeMap的高级特性,让你的代码更强大!
53 2
|
26天前
|
存储 Java API
键值对魔法:如何优雅地使用Java Map,让代码更简洁?
键值对魔法:如何优雅地使用Java Map,让代码更简洁?
105 2
|
1月前
|
安全 Java API
Java 17新特性让你的代码起飞!
【10月更文挑战第4天】自Java 8发布以来,Java语言经历了多次重大更新,每一次都引入了令人兴奋的新特性,极大地提升了开发效率和代码质量。本文将带你从Java 8一路走到Java 17,探索那些能让你的代码起飞的关键特性。
75 1
|
19天前
|
XML 安全 Java
Java反射机制:解锁代码的无限可能
Java 反射(Reflection)是Java 的特征之一,它允许程序在运行时动态地访问和操作类的信息,包括类的属性、方法和构造函数。 反射机制能够使程序具备更大的灵活性和扩展性
33 5
Java反射机制:解锁代码的无限可能
|
15天前
|
jenkins Java 测试技术
如何使用 Jenkins 自动发布 Java 代码,通过一个电商公司后端服务的实际案例详细说明
本文介绍了如何使用 Jenkins 自动发布 Java 代码,通过一个电商公司后端服务的实际案例,详细说明了从 Jenkins 安装配置到自动构建、测试和部署的全流程。文中还提供了一个 Jenkinsfile 示例,并分享了实践经验,强调了版本控制、自动化测试等关键点的重要性。
48 3
|
20天前
|
存储 安全 Java
系统安全架构的深度解析与实践:Java代码实现
【11月更文挑战第1天】系统安全架构是保护信息系统免受各种威胁和攻击的关键。作为系统架构师,设计一套完善的系统安全架构不仅需要对各种安全威胁有深入理解,还需要熟练掌握各种安全技术和工具。
59 10
|
1月前
|
存储 缓存 Java
java基础:IO流 理论与代码示例(详解、idea设置统一utf-8编码问题)
这篇文章详细介绍了Java中的IO流,包括字符与字节的概念、编码格式、File类的使用、IO流的分类和原理,以及通过代码示例展示了各种流的应用,如节点流、处理流、缓存流、转换流、对象流和随机访问文件流。同时,还探讨了IDEA中设置项目编码格式的方法,以及如何处理序列化和反序列化问题。
67 1
java基础:IO流 理论与代码示例(详解、idea设置统一utf-8编码问题)
|
16天前
|
分布式计算 Java MaxCompute
ODPS MR节点跑graph连通分量计算代码报错java heap space如何解决
任务启动命令:jar -resources odps-graph-connect-family-2.0-SNAPSHOT.jar -classpath ./odps-graph-connect-family-2.0-SNAPSHOT.jar ConnectFamily 若是设置参数该如何设置
|
14天前
|
Java
Java代码解释++i和i++的五个主要区别
本文介绍了前缀递增(++i)和后缀递增(i++)的区别。两者在独立语句中无差异,但在赋值表达式中,i++ 返回原值,++i 返回新值;在复杂表达式中计算顺序不同;在循环中虽结果相同但使用方式有别。最后通过 `Counter` 类模拟了两者的内部实现原理。
Java代码解释++i和i++的五个主要区别