代码重构专题文章
@[TOC]
九、重新组织数据
0 前言
在软件开发中,我们常常会发现,随着项目的演进,代码库会变得越来越复杂,数据结构也会越来越臃肿。不良的数据组织不仅会影响代码的可读性,更可能导致潜在的bug。本篇博客将深入探讨"重新组织数据"这一重构策略,通过一系列行之有效的方法,帮助你优化数据结构,提升代码质量。
数据重构是软件维护和进化的一个重要环节。它不仅仅是代码层面的修改,更是对系统设计深层思考的体现。通过合理地重构数据,我们可以让代码更清晰、更健壮,从而提高开发效率和系统稳定性。
1. 拆分变量(Split Variable)/移除对参数的赋值(Remove Assignments to Parameters)/分解临时变量(Split Temp)
概念阐述: 除了"循环变量"和"结果收集变量"这两种特殊情况,代码中很多变量都用于保存一段冗长代码的运算结果,以便稍后使用。理想情况下,这种变量应该只被赋值一次。如果它们被赋值超过一次,这通常意味着它们在函数中承担了一个以上的责任。当一个变量肩负多重职责时,它就会变得难以理解和维护。此时,我们应该将其替换(分解)为多个变量,每个变量只承担一个单一的、清晰的责任。这不仅能提高代码的可读性,也能降低理解复杂逻辑的认知负担。
Java 代码示例:
java
// 重构前:一个变量承担多个职责
public double calculateTotalAmountBeforeRefactor(double price, int quantity, double discountRate) {
double temp = price * quantity; // 第一次赋值:计算原始总价
if (temp > 1000) {
temp = temp * (1 - discountRate); // 第二次赋值:应用折扣
}
temp = temp + 50; // 第三次赋值:加上运费
return temp;
}
// 重构后:拆分变量,每个变量只承担一个责任
public double calculateTotalAmountAfterRefactor(double price, int quantity, double discountRate) {
double baseAmount = price * quantity; // 责任1:计算原始总价
double discountedAmount;
if (baseAmount > 1000) {
discountedAmount = baseAmount * (1 - discountRate); // 责任2:应用折扣
} else {
discountedAmount = baseAmount;
}
double finalAmount = discountedAmount + 50; // 责任3:加上运费
return finalAmount;
}
解释: 在重构前的 calculateTotalAmountBeforeRefactor
方法中,temp
变量被多次赋值,分别承担了计算原始总价、应用折扣和加上运费的职责。这使得代码难以一眼看出每个计算步骤的含义。重构后的 calculateTotalAmountAfterRefactor
方法将 temp
拆分成了 baseAmount
、discountedAmount
和 finalAmount
,每个变量都清晰地表达了其所代表的含义,代码的意图也因此变得一目了然。
2. 字段改名(Rename Field)
概念阐述: 命名是编程艺术中至关重要的一环,尤其对于程序中广泛使用的记录结构,其中字段的命名更是举足轻重。一个清晰、富有表达力的命名可以极大地提升代码的可读性和可维护性。数据结构及其字段的命名,是帮助阅读者快速理解程序意图的关键。如果字段的命名模糊、误导或不一致,那么即使是最简单的逻辑也会变得难以捉摸。因此,定期审视并改进字段命名,是提升代码质量不可或缺的一步。
Java 代码示例:
java
// 重构前:字段命名不够清晰
class CustomerBeforeRefactor {
private String nm; // 客户姓名
private String addr; // 客户地址
private String ph; // 客户电话号码
// 构造函数和Getter/Setter方法...
public CustomerBeforeRefactor(String nm, String addr, String ph) {
this.nm = nm;
this.addr = addr;
this.ph = ph;
}
public String getNm() { return nm; }
public void setNm(String nm) { this.nm = nm; }
public String getAddr() { return addr; }
public void setAddr(String addr) { this.addr = addr; }
public String getPh() { return ph; }
public void setPh(String ph) { this.ph = ph; }
}
// 重构后:字段命名清晰明了
class CustomerAfterRefactor {
private String name; // 客户姓名
private String address; // 客户地址
private String phoneNumber; // 客户电话号码
// 构造函数和Getter/Setter方法...
public CustomerAfterRefactor(String name, String address, String phoneNumber) {
this.name = name;
this.address = address;
this.phoneNumber = phoneNumber;
}
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public String getAddress() { return address; }
public void setAddress(String address) { this.address = address; }
public String getPhoneNumber() { return phoneNumber; }
public void setPhoneNumber(String phoneNumber) { this.phoneNumber = phoneNumber; }
}
解释: 在重构前的 CustomerBeforeRefactor
类中,字段 nm
、addr
和 ph
的命名过于简短和模糊。重构后的 CustomerAfterRefactor
类将它们改名为 name
、address
和 phoneNumber
。这些新名称更具描述性,使得代码的意图一目了然,即便是不熟悉代码库的人也能轻松理解每个字段的用途。
3. 以查询取代派生变量(Replace Derived Variable with Query)
概念阐述: 有些变量其实可以很容易地随时计算出来,它们的值是根据其他数据派生出来的。如果能去掉这些派生变量,也就朝着消除可变性的方向迈出了一大步。计算常能更清晰地表达数据的含义,因为它直接揭示了变量的来源和逻辑关系。更重要的是,通过实时计算而不是存储派生变量,我们可以避免"源数据修改时忘了更新派生变量"的常见错误,从而确保数据的一致性和准确性。这种重构手法有助于简化数据模型,减少状态,并提高系统的可靠性。
Java 代码示例:
java
// 重构前:使用派生变量存储折扣价
class ProductBeforeRefactor {
private double price;
private int quantity;
private double discountedPrice; // 派生变量
public ProductBeforeRefactor(double price, int quantity) {
this.price = price;
this.quantity = quantity;
this.discountedPrice = calculateDiscountedPrice(); // 在构造函数中计算并存储
}
private double calculateDiscountedPrice() {
// 假设折扣逻辑:如果总价超过100,则打9折
double total = this.price * this.quantity;
if (total > 100) {
return total * 0.9;
}
return total;
}
public double getDiscountedPrice() {
return discountedPrice;
}
// 当price或quantity改变时,需要手动更新discountedPrice
public void setPrice(double price) {
this.price = price;
this.discountedPrice = calculateDiscountedPrice(); // 容易遗漏
}
public void setQuantity(int quantity) {
this.quantity = quantity;
this.discountedPrice = calculateDiscountedPrice(); // 容易遗漏
}
}
// 重构后:以查询取代派生变量
class ProductAfterRefactor {
private double price;
private int quantity;
public ProductAfterRefactor(double price, int quantity) {
this.price = price;
this.quantity = quantity;
}
public double getPrice() { return price; }
public void setPrice(double price) { this.price = price; }
public int getQuantity() { return quantity; }
public void setQuantity(int quantity) { this.quantity = quantity; }
// 通过查询实时计算折扣价
public double getDiscountedPrice() { // 查询方法
double total = this.price * this.quantity;
if (total > 100) {
return total * 0.9;
}
return total;
}
}
解释: 在重构前的 ProductBeforeRefactor
类中,discountedPrice
是一个派生变量,它的值在构造函数中计算并存储。如果 price
或 quantity
发生变化,discountedPrice
必须手动更新,这很容易导致数据不一致。重构后的 ProductAfterRefactor
类移除了 discountedPrice
字段,而是提供了 getDiscountedPrice()
查询方法,每次调用时实时计算折扣价。这样就消除了数据不一致的风险,并简化了类的状态。
4. 将引用对象改为值对象(Change Reference to Value)
概念阐述: 在把一个对象(或数据结构)嵌入另一个对象时,位于内部的这个对象可以被视为引用对象,也可以被视为值对象。两者最明显的差异在于如何更新内部对象的属性:如果将内部对象视为引用对象,在更新其属性时,我会保留原对象不动,更新内部对象的属性;如果将其视为值对象,我就会替换整个内部对象,新换上的对象会有我想要的属性值。
如果把一个字段视为值对象,我可以把内部对象的类也变成值对象。值对象通常更容易理解,主要因为它们是不可变的。一般说来,不可变的数据结构处理起来更容易。我可以放心地把不可变的数据值传给程序的其他部分,而不必担心对象中包装的数据被偷偷修改。我可以在程序各处复制值对象,而不必操心维护内存链接。值对象在分布式系统和并发系统中尤为有用。
值对象和引用对象的区别也告诉我,何时不应该使用本重构手法。 如果我想在几个对象之间共享一个对象,以便几个对象都能看见对共享对象的修改,那么这个共享的对象就应该是引用。
Java 代码示例:
java
// 重构前:Address被视为引用对象
class AddressBeforeRefactor {
private String street;
private String city;
public AddressBeforeRefactor(String street, String city) {
this.street = street;
this.city = city;
}
public String getStreet() { return street; }
public void setStreet(String street) { this.street = street; } // 可变方法
public String getCity() { return city; }
public void setCity(String city) { this.city = city; } // 可变方法
}
class CustomerBeforeRefactor {
private String name;
private AddressBeforeRefactor address; // 引用对象
public CustomerBeforeRefactor(String name, AddressBeforeRefactor address) {
this.name = name;
this.address = address;
}
public AddressBeforeRefactor getAddress() { return address; }
// 如果直接修改get到的address对象,会影响到所有持有该引用的地方
}
// 重构后:Address被视为值对象(不可变)
final class AddressAfterRefactor { // final类,确保不可继承
private final String street; // final字段,确保不可变
private final String city; // final字段,确保不可变
public AddressAfterRefactor(String street, String city) {
this.street = street;
this.city = city;
}
public String getStreet() { return street; }
public String getCity() { return city; }
// 没有setter方法,确保不可变性
// 如果需要修改,则返回一个新的Address对象
public AddressAfterRefactor withStreet(String newStreet) {
return new AddressAfterRefactor(newStreet, this.city);
}
public AddressAfterRefactor withCity(String newCity) {
return new AddressAfterRefactor(this.street, newCity);
}
@Override
public boolean equals(Object o) { // 重要的equals和hashCode实现
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
AddressAfterRefactor that = (AddressAfterRefactor) o;
return street.equals(that.street) && city.equals(that.city);
}
@Override
public int hashCode() {
return Objects.hash(street, city);
}
}
class CustomerAfterRefactor {
private String name;
private AddressAfterRefactor address; // 值对象
public CustomerAfterRefactor(String name, AddressAfterRefactor address) {
this.name = name;
this.address = address;
}
public AddressAfterRefactor getAddress() { return address; }
// 如果需要修改地址,需要替换整个Address对象
public void setAddress(AddressAfterRefactor newAddress) {
this.address = newAddress;
}
// 示例:更新地址
public void updateStreet(String newStreet) {
this.address = this.address.withStreet(newStreet);
}
}
解释: 在重构前的代码中,AddressBeforeRefactor
是可变的,CustomerBeforeRefactor
持有其引用。这意味着通过 customer.getAddress().setStreet("...")
可能会在不经意间修改其他地方引用的同一个地址对象,导致难以追踪的副作用。
重构后的 AddressAfterRefactor
被设计为不可变的值对象:它被声明为 final
类,所有字段也是 final
,并且没有提供 setter
方法。如果需要修改地址,就必须创建一个新的 AddressAfterRefactor
实例并替换掉原来的。这样 CustomerAfterRefactor
总是持有完整的地址对象副本,而不是一个共享的引用。这大大简化了数据流的理解和并发编程中的安全性。同时,值对象需要正确实现 equals()
和 hashCode()
方法,以便进行基于值内容的比较。
5. 将值对象改为引用对象(Change Value to Reference)
概念阐述: 一个数据结构中可能包含多个记录,而这些记录都关联到同一个逻辑数据实体。例如,我可能会读取一系列订单数据,其中有多条订单属于同一个顾客。遇到这样的共享关系时,既可以把顾客信息作为值对象来看待,也可以将其视为引用对象。如果将其视为值对象,那么每份订单数据中都会复制顾客的数据;而如果将其视为引用对象,对于一个顾客,就只有一份数据结构,会有多个订单与之关联。
如果顾客数据永远不修改,那么两种处理方式都合理。把同一份数据复制多次可能会造成一点困扰,但这种情况也很常见,不会造成太大问题。过多的数据复制有可能会造成内存占用的问题,但就跟所有性能问题一样,这种情况并不常见。
如果共享的数据需要更新,将其复制多份的做法就会遇到巨大的困难。 此时我必须找到所有的副本,更新所有对象。只要漏掉一个副本没有更新,就会遭遇麻烦的数据不一致。这种情况下,可以考虑将多份数据副本变成单一的引用,这样对顾客数据的修改就会立即反映在该顾客的所有订单中。
把值对象改为引用对象会带来一个结果:对于一个客观实体,只有一个代表它的对象。这通常意味着我会需要某种形式的仓库(Repository),在仓库中可以找到所有这些实体对象。只为每个实体创建一次对象,以后始终从仓库中获取该对象。
Java 代码示例:
java
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
// 重构前:Customer被视为值对象,在每个Order中复制
class CustomerValueObject {
private String id; // 客户唯一标识
private String name;
private String email;
public CustomerValueObject(String id, String name, String email) {
this.id = id;
this.name = name;
this.email = email;
}
public String getId() { return id; }
public String getName() { return name; }
public String getEmail() { return email; }
// 值对象需要重写equals和hashCode
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
CustomerValueObject that = (CustomerValueObject) o;
return id.equals(that.id) && name.equals(that.name) && email.equals(that.email);
}
@Override
public int hashCode() {
return Objects.hash(id, name, email);
}
}
class OrderBeforeRefactor {
private String orderId;
private CustomerValueObject customer; // 每个订单都包含一份客户数据的副本
private double amount;
public OrderBeforeRefactor(String orderId, CustomerValueObject customer, double amount) {
this.orderId = orderId;
this.customer = customer; // 复制客户数据
this.amount = amount;
}
public CustomerValueObject getCustomer() { return customer; }
// 如果客户信息更新,需要找到所有相关订单并手动更新其内部的CustomerValueObject副本
// 这可能导致数据不一致
}
// 重构后:Customer被视为引用对象,通过Repository管理
class CustomerReferenceObject {
private String id;
private String name;
private String email;
public CustomerReferenceObject(String id, String name, String email) {
this.id = id;
this.name = name;
this.email = email;
}
public String getId() { return id; }
public String getName() { return name; }
public void setName(String name) { this.name = name; } // 允许修改
public String getEmail() { return email; }
public void setEmail(String email) { this.email = email; } // 允许修改
// 引用对象通常只通过ID来判断相等性
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
CustomerReferenceObject that = (CustomerReferenceObject) o;
return id.equals(that.id);
}
@Override
public int hashCode() {
return Objects.hash(id);
}
}
// 客户仓库,用于管理和获取唯一的客户实体
class CustomerRepository {
private static final Map<String, CustomerReferenceObject> customers = new HashMap<>();
public static CustomerReferenceObject getCustomerById(String id) {
return customers.get(id);
}
public static void addCustomer(CustomerReferenceObject customer) {
customers.put(customer.getId(), customer);
}
}
class OrderAfterRefactor {
private String orderId;
private String customerId; // 只存储客户ID
private double amount;
public OrderAfterRefactor(String orderId, String customerId, double amount) {
this.orderId = orderId;
this.customerId = customerId;
this.amount = amount;
}
// 通过Repository获取客户引用
public CustomerReferenceObject getCustomer() {
return CustomerRepository.getCustomerById(customerId);
}
// 示例使用
public static void main(String[] args) {
// 创建并添加到仓库
CustomerReferenceObject customer1 = new CustomerReferenceObject("C001", "Alice", "alice@example.com");
CustomerRepository.addCustomer(customer1);
// 创建订单,引用同一个客户
OrderAfterRefactor order1 = new OrderAfterRefactor("O001", "C001", 100.0);
OrderAfterRefactor order2 = new OrderAfterRefactor("O002", "C001", 250.0);
System.out.println("Order1 customer name: " + order1.getCustomer().getName());
System.out.println("Order2 customer name: " + order2.getCustomer().getName());
// 修改客户信息
customer1.setName("Alice Smith");
customer1.setEmail("alice.smith@example.com");
// 订单获取的客户信息会自动更新
System.out.println("After update - Order1 customer name: " + order1.getCustomer().getName());
System.out.println("After update - Order2 customer email: " + order2.getCustomer().getEmail());
}
}
解释: 在重构前的代码中,OrderBeforeRefactor
类中直接包含了 CustomerValueObject
的实例。这意味着如果一个客户有多个订单,其信息会在每个订单中被复制一份。当客户的姓名或邮箱需要更新时,必须遍历所有订单并手动更新每个副本,这极易导致数据不一致。
重构后的代码将 CustomerReferenceObject
设计为引用对象,它具有唯一的ID,并允许修改其属性。同时,引入 CustomerRepository
来管理 CustomerReferenceObject
的唯一实例。OrderAfterRefactor
类不再直接包含 CustomerReferenceObject
的副本,而是只存储 customerId
。当需要获取客户信息时,它通过 CustomerRepository
根据 customerId
获取 CustomerReferenceObject
的唯一引用。这样,对 CustomerReferenceObject
的任何修改都会立即反映在所有引用它的 OrderAfterRefactor
实例中,从而确保了数据的一致性。这种模式在处理需要共享和更新的实体时非常有用。
总结
数据重构不仅仅是变量命名或字段调整,更是为了提升代码的可维护性和可扩展性。通过拆分变量、合理命名、移除冗余派生变量,以及在值对象与引用对象之间做出合理选择,我们能够构建出更健壮、更清晰的代码结构。