WEBKT

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

37 0 0 0

1. 什么是 Anti-Patterns?

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

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

3.1 God Class(上帝类)

3.2 Large Class(巨型类)

3.3 Long Method(过长方法)

3.4 Feature Envy(依恋情结)

3.5 Data Clumps(数据泥团)

3.6 Shotgun Surgery(散弹式修改)

3.7 Divergent Change(发散式变化)

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

3.9 Lazy Class(冗赘类)

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

4. 重构的策略和技巧

5. 总结

各位身经百战的程序员,大家好!相信你们都曾或正在与“遗留系统”搏斗。它们像一头沉睡的巨龙,代码腐化、技术债务缠身,稍微动一下就可能引发难以预料的连锁反应。今天,我就来和大家一起深入探讨大型遗留系统中常见的 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代码优化

评论点评

打赏赞助
sponsor

感谢您的支持让我们更好的前行

分享

QRcode

https://www.webkt.com/article/9031