重构手法——数据重组类 | 数据关系重构 | 将值更改为引用

76 阅读7分钟

简介

“Change Value to Reference”(将值更改为引用)是一种重构手法,当多个值对象具有相同的状态且你希望避免重复存储相同数据时,此重构方法较为适用。通过将值对象转换为引用对象,可以减少内存消耗,并便于对共享状态进行集中管理。以下是进行“Change Value to Reference”重构的详细步骤。

针对的症状(代码坏味道)

  • 值对象重复:存在多个值对象,它们具有相同的状态,导致数据冗余。
  • 需要共享状态:希望对这些具有相同状态的值对象进行集中管理,共享它们的状态。

“Change Value to Reference”的详细步骤

  1. 识别值对象
    • 查找重复值对象:在代码中寻找具有相同状态的多个值对象。
    • 评估共享可能性:判断这些值对象是否可以通过共享状态来优化,确保共享状态不会对原有功能产生负面影响。
  2. 创建引用对象
    • 设计引用类:创建一个新的类作为引用对象,该类应包含值对象中的所有状态属性。
    • 提供必要方法:为引用类提供访问和修改状态的方法,例如getter和setter方法。
  3. 替换值对象为引用对象
    • 更新数据结构:在使用值对象的地方,将其替换为引用对象。这可能涉及到修改类的成员变量类型、方法参数和返回值类型等。
    • 调整相关逻辑:由于引用对象的特性,可能需要修改与值对象交互的逻辑,例如比较对象时从比较值改为比较引用。
  4. 管理引用对象的生命周期
    • 对象创建与缓存:决定如何创建引用对象,并考虑是否需要使用缓存机制来管理引用对象的实例,以确保相同状态的对象只有一个实例。
    • 内存管理:确保在不再需要引用对象时,正确处理其内存释放,避免内存泄漏。
  5. 测试
    • 编译代码:确保代码编译通过,没有任何语法错误。
    • 运行测试:运行所有相关的单元测试,确保重构操作没有引入新的错误。
    • 手动测试:如果有必要,进行手动测试以验证功能的正确性,特别是涉及到对象引用和共享状态的部分。
  6. 代码审查
    • 同行评审:让同事或其他团队成员审查你的更改,确保代码质量和可维护性没有下降,特别是在处理对象引用和共享状态方面。
    • 文档更新:如果项目有维护文档的习惯,记得更新相关文档,说明将值更改为引用的影响。

示例

假设我们有一个AddressValue类,当前以值对象的形式被多个Customer对象使用,并且存在许多重复的地址信息,我们希望将其更改为引用对象。

原始代码(值对象形式)

class AddressValue {
    private String street;
    private String city;

    public AddressValue(String street, String city) {
        this.street = street;
        this.city = city;
    }

    public String getStreet() {
        return street;
    }

    public String getCity() {
        return city;
    }
}

class Customer {
    private AddressValue address;

    public Customer(AddressValue address) {
        this.address = address;
    }

    public AddressValue getAddress() {
        return address;
    }
}

重构步骤

  1. 识别值对象AddressValue类是值对象,并且多个Customer对象可能拥有相同的地址,存在数据冗余。

  2. 创建引用对象

    • 创建AddressReference类:
        class AddressReference {
           private String street;
           private String city;
      
           public AddressReference(String street, String city) {
              this.street = street;
              this.city = city;
           }
      
           public String getStreet() {
              return street;
           }
      
           public String getCity() {
              return city;
           }
        }
      
  3. 替换值对象为引用对象:修改Customer类,使用AddressReference代替AddressValue

       class Customer {
           private AddressReference address;
    
        public Customer(AddressReference address) {
            this.address = address;
        }
    
        public AddressReference getAddress() {
            return address;
        }
       }
    
  4. 管理引用对象的生命周期:可以考虑使用一个AddressRegistry类来管理AddressReference对象的创建和缓存,例如:

       class AddressRegistry {
       private static Map<String, AddressReference> addressCache = new HashMap<>();
    
       public static AddressReference getAddress(String street, String city) {
          String key = street + "_" + city;
          if (!addressCache.containsKey(key)) {
             AddressReference address = new AddressReference(street, city);
             addressCache.put(key, address);
          }
          return addressCache.get(key);
       }
    }
    

    在创建Customer对象时,通过AddressRegistry获取AddressReference对象:

       public class Main {
       public static void main(String[] args) {
          AddressReference address1 = AddressRegistry.getAddress("123 Main St", "Anytown");
          AddressReference address2 = AddressRegistry.getAddress("123 Main St", "Anytown");
          Customer customer1 = new Customer(address1);
          Customer customer2 = new Customer(address2);
          // address1 和 address2 是同一个对象
       }
    }
    
  5. 测试

    • 编译代码,确保没有语法错误。
    • 运行相关单元测试,验证功能是否正常,特别是AddressRegistry的缓存功能以及Customer类与AddressReference类的交互。
  6. 代码审查

    • 邀请同事审查代码,确保在处理对象引用和共享状态方面没有引入新问题。
    • 更新文档,说明Address已从值对象改为引用对象,并介绍AddressRegistry的使用。

练习

基础练习题

  1. 简单值对象转换

    • 给定以下 Java 代码,ColorValue类作为值对象被Shape类使用。请将ColorValue类转换为引用对象ColorReference

        class ColorValue {
         private String name;
         private int red;
         private int green;
         private int blue;
      
         public ColorValue(String name, int red, int green, int blue) {
            this.name = name;
            this.red = red;
            this.green = green;
            this.blue = blue;
         }
      
         public String getName() {
            return name;
         }
      
         public int getRed() {
            return red;
         }
      
         public int getGreen() {
            return green;
         }
      
         public int getBlue() {
            return blue;
         }
      }
      
      class Shape {
         private ColorValue color;
      
         public Shape(ColorValue color) {
            this.color = color;
         }
      
         public ColorValue getColor() {
            return color;
         }
      }
      
  2. 多层值对象转换

    • 下面的 Java 代码中有Product类使用CategoryValue类,CategoryValue类又使用SubCategoryValue类。请将 SubCategoryValue类和CategoryValue类转换为引用对象SubCategoryReferenceCategoryReference,并相应调整 Product类。

        class SubCategoryValue {
          private String name;
      
          public SubCategoryValue(String name) {
              this.name = name;
          }
      
          public String getName() {
              return name;
          }
      }
      
      class CategoryValue {
          private String name;
          private SubCategoryValue subCategory;
      
          public CategoryValue(String name, SubCategoryValue subCategory) {
              this.name = name;
              this.subCategory = subCategory;
          }
      
          public String getName() {
              return name;
          }
      
          public SubCategoryValue getSubCategory() {
              return subCategory;
          }
      }
      
      class Product {
          private String name;
          private CategoryValue category;
      
          public Product(String name, CategoryValue category) {
              this.name = name;
              this.category = category;
          }
      
          public String getName() {
              return name;
          }
      
          public CategoryValue getCategory() {
              return category;
          }
      }
      

进阶练习题

  1. 值对象转换与逻辑调整

    • 在这段 Java 代码中,OrderItem类使用ProductValue类,并且在Order类的calculateTotal方法中有基于ProductValue 的逻辑。请将 ProductValue类转换为引用对象ProductReference,并调整calculateTotal方法的逻辑。

        class ProductValue {
         private String name;
         private double price;
      
         public ProductValue(String name, double price) {
            this.name = name;
            this.price = price;
         }
      
         public String getName() {
            return name;
         }
      
         public double getPrice() {
            return price;
         }
      }
      
      class OrderItem {
         private ProductValue product;
         private int quantity;
      
         public OrderItem(ProductValue product, int quantity) {
            this.product = product;
            this.quantity = quantity;
         }
      
         public ProductValue getProduct() {
            return product;
         }
      
         public int getQuantity() {
            return quantity;
         }
      }
      
      class Order {
         private List<OrderItem> items;
      
         public Order(List<OrderItem> items) {
            this.items = items;
         }
      
         public double calculateTotal() {
            double total = 0;
            for (OrderItem item : items) {
               total += item.getProduct().getPrice() * item.getQuantity();
            }
            return total;
         }
      }
      
  2. 引用对象的缓存与管理

    • 给定以下 Java 代码,EmployeeValue类作为值对象被Department类使用。请将EmployeeValue类转换为引用对象 EmployeeReference,并实现一个缓存机制来管理EmployeeReference对象的实例,同时确保在Department类中正确使用缓存的对象。

        class EmployeeValue {
         private String name;
         private int age;
      
         public EmployeeValue(String name, int age) {
            this.name = name;
            this.age = age;
         }
      
         public String getName() {
            return name;
         }
      
         public int getAge() {
            return age;
         }
      }
      
      class Department {
         private List<EmployeeValue> employees;
      
         public Department(List<EmployeeValue> employees) {
            this.employees = employees;
         }
      
         public List<EmployeeValue> getEmployees() {
            return employees;
         }
      }
      

综合拓展练习题

  1. 多类值对象重构与代码审查模拟
    • 考虑一个简单的 Java 在线商城系统,有Product类、Order类、Cart类。Product类使用BrandValue类和CategoryValue 类作为值对象,Order类和Cart类都包含处理Product对象的逻辑。
    • 请对这些类进行“Change Value to Reference”重构,将BrandValue类和CategoryValue类转换为引用对象,并调整相关类的逻辑。
    • 假设你完成了重构,请模拟一份简单的代码审查报告,指出重构后的优点和可能存在的潜在问题。
        class BrandValue {
         private String name;
         private String countryOfOrigin;
      
         public BrandValue(String name, String countryOfOrigin) {
            this.name = name;
            this.countryOfOrigin = countryOfOrigin;
         }
      
         public String getName() {
            return name;
         }
      
         public String getCountryOfOrigin() {
            return countryOfOrigin;
         }
      }
      
      class CategoryValue {
         private String name;
      
         public CategoryValue(String name) {
            this.name = name;
         }
      
         public String getName() {
            return name;
         }
      }
      
      class Product {
         private String name;
         private BrandValue brand;
         private CategoryValue category;
         private double price;
      
         public Product(String name, BrandValue brand, CategoryValue category, double price) {
            this.name = name;
            this.brand = brand;
            this.category = category;
            this.price = price;
         }
      
         public String getName() {
            return name;
         }
      
         public BrandValue getBrand() {
            return brand;
         }
      
         public CategoryValue getCategory() {
            return category;
         }
      
         public double getPrice() {
            return price;
         }
      }
      
      class Cart {
         private List<Product> products;
      
         public Cart(List<Product> products) {
            this.products = products;
         }
      
         public double calculateTotal() {
            double total = 0;
            for (Product product : products) {
               total += product.getPrice();
            }
            return total;
         }
      }
      
      class Order {
         private List<Product> products;
      
         public Order(List<Product> products) {
            this.products = products;
         }
      
         public double calculateTotal() {
            double total = 0;
            for (Product product : products) {
               total += product.getPrice();
            }
            return total;
         }
      }