(五)数据重构的艺术:优化你的代码结构与可读性

代码重构专题文章

代码重构精要:提升代码品质的艺术(汇总篇)

(一)代码匠心:重构之道,化腐朽为神奇

(二)重构的艺术:精进代码的第一组基本功

(三)封装与结构优化:让代码更优雅

(四)优雅重构:洞悉"搬移特性"的艺术与实践

(五)数据重构的艺术:优化你的代码结构与可读性

(六)重构的艺术:简化复杂条件逻辑的秘诀

(七)API 重构的艺术:打造优雅、可维护的 API

(八)掌握继承的艺术:重构之路,化繁为简

@[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 拆分成了 baseAmountdiscountedAmountfinalAmount,每个变量都清晰地表达了其所代表的含义,代码的意图也因此变得一目了然。

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 类中,字段 nmaddrph 的命名过于简短和模糊。重构后的 CustomerAfterRefactor 类将它们改名为 nameaddressphoneNumber。这些新名称更具描述性,使得代码的意图一目了然,即便是不熟悉代码库的人也能轻松理解每个字段的用途。

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 是一个派生变量,它的值在构造函数中计算并存储。如果 pricequantity 发生变化,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 实例中,从而确保了数据的一致性。这种模式在处理需要共享和更新的实体时非常有用。

总结

数据重构不仅仅是变量命名或字段调整,更是为了提升代码的可维护性和可扩展性。通过拆分变量、合理命名、移除冗余派生变量,以及在值对象与引用对象之间做出合理选择,我们能够构建出更健壮、更清晰的代码结构。

参考

《重构:改善既有代码的设计(第二版)》

相关推荐
fatfishccc4 小时前
(八)掌握继承的艺术:重构之路,化繁为简
代码规范
fatfishccc4 小时前
(六)重构的艺术:简化复杂条件逻辑的秘诀
代码规范
帅次1 天前
系统分析师-软件工程-信息系统开发方法&面向对象&原型化方法&面向服务&快速应用开发
软件工程·团队开发·软件构建·需求分析·代码规范·敏捷流程·结对编程
San302 天前
JavaScript 流程控制与数组操作全解析:从条件判断到数据高效处理
javascript·面试·代码规范
文心快码BaiduComate3 天前
再获殊荣!文心快码荣膺2025年度优秀软件产品!
前端·后端·代码规范
许雪里9 天前
XXL-TOOL v2.1.0 发布 | Java工具类库
后端·github·代码规范
召摇10 天前
如何避免写垃圾代码:Java篇
java·后端·代码规范
那个下雨天14 天前
护城河式编程模式:黑色幽默中的工程生存学
职场发展·代码规范·护城河式编程·职场心得
想用offer打牌15 天前
线程池踩坑之一:将其放在类的成员变量
后端·面试·代码规范