WEBKT

大型遗留系统常见 Anti-Patterns 及重构方案实战解析

90 0 0 0

各位身经百战的程序员,大家好!相信你们都曾或正在与“遗留系统”搏斗。它们像一头沉睡的巨龙,代码腐化、技术债务缠身,稍微动一下就可能引发难以预料的连锁反应。今天,我就来和大家一起深入探讨大型遗留系统中常见的 Anti-Patterns,并分享一些实战性较强的重构方案,助你驯服这头巨龙。

1. 什么是 Anti-Patterns?

Anti-Patterns,顾名思义,是反模式。它指的是在软件开发过程中,一些看似合理但实际上会导致问题,甚至会适得其反的常见做法。这些模式往往一开始能解决一些燃眉之急,但随着时间的推移,会逐渐积累技术债务,最终拖垮整个系统。

2. 遗留系统 Anti-Patterns 的重灾区

遗留系统往往是 Anti-Patterns 的重灾区,这主要是因为:

  • 缺乏文档和清晰的架构: 随着人员的更迭,最初的设计思想逐渐 lost,代码缺乏注释,架构变得混乱不堪。
  • 技术债务积累: 为了快速上线,采用了各种各样的“权宜之计”,这些“坑”随着时间推移,越积越多。
  • 对变更的恐惧: 由于系统复杂性高,风险大,团队对任何变更都小心翼翼,甚至不敢轻易触碰。

3. 常见的 Anti-Patterns 及重构方案

接下来,我们就来详细剖析一些大型遗留系统中常见的 Anti-Patterns,并针对性地给出重构方案。

3.1 God Class(上帝类)

描述: God Class 指的是一个类承担了过多的职责,拥有过多的属性和方法。它就像一个“万能钥匙”,几乎所有的业务逻辑都集中在这个类中。这会导致代码高度耦合,难以维护和测试。

问题:

  • 高耦合: God Class 与系统中的其他类紧密耦合,修改其中一部分代码可能会影响到其他模块。
  • 低内聚: 类的职责不单一,代码可读性差,难以理解和维护。
  • 难以测试: 由于类的复杂性,单元测试变得非常困难。

重构方案:

  • Single Responsibility Principle (SRP): 将 God Class 拆分成多个类,每个类只负责一个单一的职责。可以使用 Extract Class 手法将相关的属性和方法提取到新的类中。
  • Introduce Design Patterns: 引入设计模式,例如 Strategy、Command、Observer 等,将不同的业务逻辑委托给不同的类来处理。这可以降低类的复杂度,提高代码的可扩展性。

示例:

假设有一个 OrderService 类,负责处理订单的创建、支付、发货等所有业务逻辑。可以将其拆分成 OrderCreationServicePaymentServiceShippingService 等类,每个类只负责一个特定的任务。

// 重构前
public class OrderService {
    public void createOrder(Order order) { ... }
    public void processPayment(Order order) { ... }
    public void shipOrder(Order order) { ... }
}

// 重构后
public class OrderCreationService {
    public void createOrder(Order order) { ... }
}

public class PaymentService {
    public void processPayment(Order order) { ... }
}

public class ShippingService {
    public void shipOrder(Order order) { ... }
}

3.2 Large Class(巨型类)

描述: Large Class 指的是一个类代码行数过多,通常超过几千甚至上万行。这通常是 God Class 的一种表现形式,也可能是由于历史原因,不断地向类中添加新的功能造成的。

问题:

  • 可读性差: 代码难以阅读和理解,维护成本高。
  • 查找困难: 在大量的代码中查找特定的功能或 Bug 犹如大海捞针。
  • 编译时间长: 编译大型类需要更长的时间,影响开发效率。

重构方案:

  • Extract Class: 将类中相关的属性和方法提取到新的类中,遵循 SRP 原则。
  • Extract Subclass: 如果类中的某些功能只在特定的情况下使用,可以将其提取到子类中,使用继承来降低类的复杂度。
  • Extract Interface: 将类的接口定义提取到接口中,使用接口来隔离不同的实现。这可以提高代码的灵活性和可测试性。

示例:

假设有一个 ReportGenerator 类,负责生成各种类型的报表,例如销售报表、财务报表等。可以将其拆分成 SalesReportGeneratorFinancialReportGenerator 等类,每个类负责生成特定类型的报表。

// 重构前
public class ReportGenerator {
    public void generateSalesReport(Date startDate, Date endDate) { ... }
    public void generateFinancialReport(Date startDate, Date endDate) { ... }
}

// 重构后
public interface ReportGenerator {
    void generateReport(Date startDate, Date endDate);
}

public class SalesReportGenerator implements ReportGenerator {
    @Override
    public void generateReport(Date startDate, Date endDate) { ... }
}

public class FinancialReportGenerator implements ReportGenerator {
    @Override
    public void generateReport(Date startDate, Date endDate) { ... }
}

3.3 Long Method(过长方法)

描述: Long Method 指的是一个方法代码行数过多,通常超过几十甚至几百行。这通常是由于方法承担了过多的职责,或者缺乏良好的代码组织造成的。

问题:

  • 可读性差: 代码难以阅读和理解,维护成本高。
  • 难以测试: 单元测试变得非常困难,难以覆盖所有分支。
  • 代码重复: 过长的方法往往包含大量的重复代码。

重构方案:

  • Extract Method: 将方法中相关的代码块提取到新的方法中,遵循 SRP 原则。提取后的方法应该有一个清晰的名称,能够表达其功能。
  • Replace Temp with Query: 将临时变量替换为查询方法,避免在方法中定义过多的临时变量。
  • Introduce Parameter Object: 如果方法需要传递多个参数,可以将其封装成一个参数对象,提高代码的可读性和可维护性。

示例:

假设有一个 calculateTotalPrice 方法,负责计算订单的总价,包括商品价格、运费、折扣等。可以将其拆分成 calculateProductPricecalculateShippingFeecalculateDiscount 等方法,每个方法负责计算一个特定的价格。

// 重构前
public double calculateTotalPrice(List<OrderItem> items, double shippingFee, double discountRate) {
    double productPrice = 0;
    for (OrderItem item : items) {
        productPrice += item.getPrice() * item.getQuantity();
    }
    double discount = productPrice * discountRate;
    return productPrice + shippingFee - discount;
}

// 重构后
public double calculateTotalPrice(List<OrderItem> items, double shippingFee, double discountRate) {
    double productPrice = calculateProductPrice(items);
    double discount = calculateDiscount(productPrice, discountRate);
    return productPrice + shippingFee - discount;
}

private double calculateProductPrice(List<OrderItem> items) {
    double productPrice = 0;
    for (OrderItem item : items) {
        productPrice += item.getPrice() * item.getQuantity();
    }
    return productPrice;
}

private double calculateDiscount(double productPrice, double discountRate) {
    return productPrice * discountRate;
}

3.4 Feature Envy(依恋情结)

描述: Feature Envy 指的是一个方法过于依赖其他类的数据和方法,仿佛它更应该存在于那个类中。这会导致代码分布不合理,增加代码的耦合度。

问题:

  • 高耦合: 方法与其他的类紧密耦合,修改其中一部分代码可能会影响到其他模块。
  • 代码分布不合理: 代码没有放在最适合它的地方,可读性差。
  • 违反封装性: 方法访问了其他类过多的内部数据,破坏了封装性。

重构方案:

  • Move Method: 将方法移动到最适合它的类中。这可以提高代码的内聚性,降低代码的耦合度。
  • Extract Method + Move Method: 如果方法只使用了其他类的一部分数据,可以先将这部分代码提取到新的方法中,然后再将方法移动到那个类中。

示例:

假设有一个 Customer 类和一个 Order 类,Order 类中有一个 calculateDiscount 方法,该方法需要访问 Customer 类的 会员等级 属性才能计算折扣。可以将 calculateDiscount 方法移动到 Customer 类中。

// 重构前
public class Order {
    private Customer customer;

    public double calculateDiscount(double price) {
        // 依赖 Customer 类的会员等级属性
        if (customer.getMembershipLevel() == MembershipLevel.GOLD) {
            return price * 0.1;
        } else {
            return 0;
        }
    }
}

// 重构后
public class Customer {
    public double calculateDiscount(double price) {
        if (getMembershipLevel() == MembershipLevel.GOLD) {
            return price * 0.1;
        } else {
            return 0;
        }
    }
}

public class Order {
    private Customer customer;

    public double getTotalPrice() {
        double price = ...;
        return price - customer.calculateDiscount(price);
    }
}

3.5 Data Clumps(数据泥团)

描述: Data Clumps 指的是一组数据项经常一起出现,例如在多个类中都包含相同的属性,或者在多个方法中都需要传递相同的参数。这表明这些数据项之间存在某种关联,应该将它们封装成一个独立的类。

问题:

  • 代码重复: 相同的属性或参数在多个地方重复出现,增加了代码的冗余度。
  • 维护困难: 修改其中一个地方的属性或参数,需要同时修改其他所有地方,增加了维护成本。
  • 缺乏内聚性: 相关的属性或参数没有封装在一起,缺乏内聚性。

重构方案:

  • Extract Class: 将相关的数据项封装成一个独立的类。这可以减少代码的重复,提高代码的可维护性。
  • Introduce Parameter Object: 如果多个方法都需要传递相同的一组参数,可以将这些参数封装成一个参数对象。

示例:

假设在 Customer 类和 Order 类中都需要包含客户的姓名、地址和电话号码。可以将这些属性封装成一个 ContactInfo 类。

// 重构前
public class Customer {
    private String name;
    private String address;
    private String phoneNumber;
}

public class Order {
    private String customerName;
    private String customerAddress;
    private String customerPhoneNumber;
}

// 重构后
public class ContactInfo {
    private String name;
    private String address;
    private String phoneNumber;
}

public class Customer {
    private ContactInfo contactInfo;
}

public class Order {
    private ContactInfo customerContactInfo;
}

3.6 Shotgun Surgery(散弹式修改)

描述: Shotgun Surgery 指的是当需要修改一个功能时,需要在多个类中进行小范围的修改。这通常是由于代码分布不合理,或者缺乏良好的抽象造成的。

问题:

  • 维护困难: 需要修改多个地方,容易遗漏,增加了出错的风险。
  • 代码耦合度高: 修改一个功能可能会影响到其他多个模块,增加了代码的耦合度。
  • 难以理解: 代码分布在多个地方,难以理解功能的完整实现。

重构方案:

  • Move Method: 将相关的代码移动到同一个类中,提高代码的内聚性。
  • Inline Class: 如果一个类只被一个其他的类使用,可以将这个类的内容合并到那个类中。
  • Introduce Strategy Pattern: 引入策略模式,将不同的算法封装成不同的策略类,可以灵活地切换算法,而不需要修改大量的代码。

示例:

假设在系统中需要添加一种新的支付方式,需要在 OrderServicePaymentServiceNotificationService 等多个类中进行修改。可以引入策略模式,将不同的支付方式封装成不同的策略类,例如 CreditCardPaymentPayPalPayment 等。

// 重构前
public class OrderService {
    public void processPayment(Order order, String paymentType) {
        if (paymentType.equals("credit_card")) {
            // 处理信用卡支付
        } else if (paymentType.equals("paypal")) {
            // 处理 PayPal 支付
        }
    }
}

// 重构后
public interface PaymentStrategy {
    void processPayment(Order order);
}

public class CreditCardPayment implements PaymentStrategy {
    @Override
    public void processPayment(Order order) {
        // 处理信用卡支付
    }
}

public class PayPalPayment implements PaymentStrategy {
    @Override
    public void processPayment(Order order) {
        // 处理 PayPal 支付
    }
}

public class OrderService {
    public void processPayment(Order order, PaymentStrategy paymentStrategy) {
        paymentStrategy.processPayment(order);
    }
}

3.7 Divergent Change(发散式变化)

描述: Divergent Change 指的是一个类因为不同的原因需要修改。这违反了 SRP 原则,表明类承担了过多的职责。

问题:

  • 维护困难: 修改一个功能可能会影响到其他不相关的功能,增加了维护成本。
  • 代码耦合度高: 类与不同的模块耦合在一起,增加了代码的耦合度。
  • 难以理解: 类的职责不单一,难以理解类的完整功能。

重构方案:

  • Extract Class: 将类拆分成多个类,每个类只负责一个单一的职责。可以使用 Extract Class 手法将相关的属性和方法提取到新的类中。

示例:

假设有一个 Product 类,既需要负责处理商品的价格和描述信息,又需要负责处理商品的库存信息。可以将其拆分成 ProductInfo 类和 Inventory 类,ProductInfo 类负责处理商品的价格和描述信息,Inventory 类负责处理商品的库存信息。

// 重构前
public class Product {
    private String name;
    private String description;
    private double price;
    private int stockQuantity;
}

// 重构后
public class ProductInfo {
    private String name;
    private String description;
    private double price;
}

public class Inventory {
    private int stockQuantity;
}

public class Product {
    private ProductInfo productInfo;
    private Inventory inventory;
}

3.8 Parallel Inheritance Hierarchies(平行继承体系)

描述: Parallel Inheritance Hierarchies 指的是存在多个平行的继承体系,当为一个继承体系添加一个新的子类时,需要在其他的继承体系中也添加相应的子类。这会导致代码的重复,增加维护成本。

问题:

  • 代码重复: 相同的代码在多个继承体系中重复出现,增加了代码的冗余度。
  • 维护困难: 需要同时维护多个继承体系,增加了维护成本。
  • 设计复杂: 多个继承体系之间存在关联,增加了设计的复杂性。

重构方案:

  • Move Method: 将相关的代码移动到同一个类中,提高代码的内聚性。
  • Replace Inheritance with Delegation: 使用委托代替继承,可以避免创建多个平行的继承体系。

示例:

假设存在一个 Shape 继承体系,包括 CircleRectangle,以及一个 Renderer 继承体系,包括 SVG RendererCanvas Renderer。如果需要添加一个新的形状,例如 Triangle,需要在 Shape 继承体系中添加 Triangle 类,并在 Renderer 继承体系中添加相应的 Triangle Renderer 类。可以使用委托代替继承,将 Renderer 作为 Shape 类的一个属性,避免创建 Renderer 继承体系。

// 重构前
public abstract class Shape {
    public abstract void draw();
}

public class Circle extends Shape {
    @Override
    public void draw() {
        // 使用 SVG Renderer 绘制圆形
    }
}

public class Rectangle extends Shape {
    @Override
    public void draw() {
        // 使用 SVG Renderer 绘制矩形
    }
}

public abstract class Renderer {
    public abstract void render(Shape shape);
}

public class SVGRenderer extends Renderer {
    @Override
    public void render(Shape shape) {
        // 使用 SVG 绘制形状
    }
}

public class CanvasRenderer extends Renderer {
    @Override
    public void render(Shape shape) {
        // 使用 Canvas 绘制形状
    }
}

// 重构后
public class Shape {
    private Renderer renderer;

    public Shape(Renderer renderer) {
        this.renderer = renderer;
    }

    public void draw() {
        renderer.render(this);
    }
}

public interface Renderer {
    void render(Shape shape);
}

public class SVGRenderer implements Renderer {
    @Override
    public void render(Shape shape) {
        // 使用 SVG 绘制形状
    }
}

public class CanvasRenderer implements Renderer {
    @Override
    public void render(Shape shape) {
        // 使用 Canvas 绘制形状
    }
}

3.9 Lazy Class(冗赘类)

描述: Lazy Class 指的是一个类没有做足够的工作,它的功能过于简单,没有存在的必要。这通常是由于过度设计造成的,或者随着系统的演变,某些类变得不再有用。

问题:

  • 增加复杂性: 过多的类会增加系统的复杂性,难以理解和维护。
  • 增加代码量: 过多的类会增加代码量,降低开发效率。

重构方案:

  • Inline Class: 将类的内容合并到其他的类中。这可以减少类的数量,降低系统的复杂性。
  • Collapse Hierarchy: 如果一个继承体系中的某些类没有做足够的工作,可以将它们合并到父类中。

示例:

假设有一个 Address 类,只包含城市和街道两个属性,并且只被 Customer 类使用。可以将 Address 类的属性合并到 Customer 类中。

// 重构前
public class Address {
    private String city;
    private String street;
}

public class Customer {
    private Address address;
}

// 重构后
public class Customer {
    private String city;
    private String street;
}

3.10 Speculative Generality(夸夸其谈未来性)

描述: Speculative Generality 指的是为了应对未来可能的需求,而过度设计代码。这会导致代码的复杂性增加,但实际上这些需求可能永远不会出现。

问题:

  • 增加复杂性: 过度设计会增加代码的复杂性,难以理解和维护。
  • 浪费时间: 花费大量时间设计和实现未来可能不会用到的功能,浪费了开发资源。
  • 代码冗余: 为了应对未来可能的需求,会添加大量的冗余代码。

重构方案:

  • You Ain't Gonna Need It (YAGNI): 只实现当前需要的功能,不要过度设计。
  • Remove Dead Code: 删除不再使用的代码。
  • Collapse Hierarchy: 如果一个继承体系中的某些类是为了应对未来可能的需求而设计的,可以将它们合并到父类中。

示例:

假设在系统中需要支持多种数据库,因此设计了一个抽象的 Database 类和多个具体的数据库实现类,例如 MySQLDatabasePostgreSQLDatabase 等。但实际上,系统只使用了 MySQL 数据库。可以将 Database 类和 PostgreSQLDatabase 类删除,只保留 MySQLDatabase 类。

4. 重构的策略和技巧

  • 小步快跑: 不要试图一次性重构整个系统,而是应该将重构任务分解成小的步骤,每次只修改一小部分代码。
  • 测试驱动: 在重构之前,先编写单元测试,确保重构后的代码功能没有改变。
  • 持续集成: 将重构后的代码集成到主干分支中,并进行自动化测试,及时发现和修复问题。
  • Code Review: 让其他团队成员参与代码审查,可以帮助发现潜在的问题,并提高代码的质量。
  • 版本控制: 使用版本控制系统,例如 Git,可以方便地回滚到之前的版本,避免重构失败造成损失。

5. 总结

遗留系统就像一座年久失修的房子,需要我们不断地维护和修缮。通过识别和重构常见的 Anti-Patterns,我们可以有效地降低系统的复杂性,提高代码的可维护性,最终驯服这头沉睡的巨龙。记住,重构是一个持续的过程,需要我们不断地学习和实践。希望这篇文章能够帮助你更好地理解和应对遗留系统带来的挑战。

祝大家编码愉快!

代码老中医 遗留系统重构Anti-Patterns代码优化

评论点评