代码重构实战-将值对象改为引用对象(Change Value to Reference)

简介: 一个数据结构中可能包含多个记录,而这些记录都关联到同一个逻辑数据结构。例如,我可能会读取一系列订单数据,其中有多条订单属于同一个顾客。遇到这样的共享关系,既能将顾客信息作为值对象看待,也能将其视为引用对象

动机


一个数据结构中可能包含多个记录,而这些记录都关联到同一个逻辑数据结构。例如,我可能会读取一系列订单数据,其中有多条订单属于同一个顾客。遇到这样的共享关系,既能将顾客信息作为值对象看待,也能将其视为引用对象:


若将其视为值对象,则每份订单数据中都会复制顾客的数据

若将其视为引用对象,对于一个顾客,就只有一份数据结构,会有多个订单与之关联

若顾客数据永不修改,则两种方式都合理。 把同一份数据复制多次可能会造成一点困扰,但这种情况也


很常见,不是太大问题。过多的数据复制有可能会造成内存占用的问题,但就跟所有性能问题一样,这种情况并不常见。


若共享的数据需要更新,将其复制多份的做法就会遇到巨大困难。此时我必须找到所有副本,更新所有对象。漏掉一个副本没更新,就会导致数据不一致。这时,考虑将多份数据副本变成单一的引用,这样对顾客数据的修改就会立即反映在该顾客的所有订单中。


把值对象改为引用对象会带来一个结果:对于一个客观实体,只有一个代表它的对象。这通常意味着我会需要某种形式的仓库,在仓库中可以找到所有这些实体对象。只为每个实体创建一次对象,以后始终从仓库中获取该对象。


做法


为相关对象创建一个仓库(若还没这样的一个仓库)。 确保构造器有办法找到关联对象的正确实例。修改宿主对象的构造器,令其从仓库中获取关联对象。每次修改后测试。


案例


订单Order类,其实例对象可从一个JSON文件创建。用来创建订单的数据中有一个顾客(customer)ID,我们用它来进一步创建Customer对象。


package com.javaedge.refactor.ttt;

import lombok.AllArgsConstructor;

import lombok.Getter;

import java.math.BigDecimal;

import java.util.Objects;

/**

* @author JavaEdge

* @date 2022/4/1

*/

@Getter

public class Order {

 

   private Customer customer;

   public Order(String customerName) {

       customer = new Customer(customerName);

   }

   public String getCustomerName() {

       return customer.getName();

   }

   void setCustomerName(String customerName) {

       customer = new Customer(customerName);

   }

}


此外,还有一些代码也会使用****Customer****对象;


package com.javaedge.refactor.ttt;


import lombok.Getter;


import java.util.Collection;


/**

* @author JavaEdge

* @date 2022/4/1

*/

@Getter

public class Order {

   private Customer customer;

   public Order(String customerName) {

       customer = new Customer(customerName);

   }

   public String getCustomerName() {

       return customer.getName();

   }

   void setCustomerName(String customerName) {

       customer = new Customer(customerName);

   }

   private static int numberOfOrdersFor(Collection<Order> orders, String customer) {

       int result = 0;

       for (Order each : orders) {

           if (each.getCustomerName().equals(customer)) {

               result++;

           }

       }

       return result;

   }

}



到目前为止,Customer对象还是值对象。就算多份订单属于同一客户,但每个****Order*对象还是拥有各自的*Customer*对象。我希望改变这现状,使得一旦同一客户拥有多份不同订单,代表这些订单的所有*Order****对象就能共享同一个Customer对象。本例中,就意味着:每一个客户名称只该对应一个Customer对象。


首先我使用 Replace Constructor with Factory Method,控制 ****Customer*对象的创建过程。我在*Customer****中定义工厂方法:


package com.javaedge.refactor.ttt;


import lombok.AllArgsConstructor;

import lombok.Getter;

/**

* @author JavaEdge

* @date 2022/4/1

*/

@Getter

@AllArgsConstructor

class Customer {

   private String name;

   public static Customer create(String name) {

       return new Customer(name);

   }

}


然后把原本调用构造函数的地方改为调用工厂函数:


然后再把构造函数声明为private:


package com.javaedge.refactor.ttt;

import lombok.AllArgsConstructor;

import lombok.Getter;

/**

* @author JavaEdge

* @date 2022/4/1

*/

@Getter

class Customer {

   private String name;

   private Customer(String name) {

       this.name = name;

   }

   public static Customer create(String name) {

       return new Customer(name);

   }

}



现在,我必须决定如何访问Customer对象。我比较喜欢通过另一个对象(例如Order中的一个字段)来访问它。但本例并没有这样一个明显的字段用于访问Customer对象。


这时,我通常会创建一个注册表对象来保存所有Customer对象,以此作为访问点。简化例子,我把这个注册表保存在Customer类的static字段中,让Customer类作为访问点:


然后我得决定:


在接到请求时,创建新的Customer对象

还是预先将它们创建好

这里我选择后者。在应用程序的启动代码中,先把需要使用的Customer对象加载妥当。这些对象可能来自数据库,也可能来自文件。简单起见,我在代码中明确生成这些对象。反正以后我总是可以使用Substitute Algorithm改变它们的创建方式。


package com.javaedge.refactor.ttt;

import lombok.AllArgsConstructor;

import lombok.Getter;

import java.util.Dictionary;

import java.util.HashMap;

import java.util.Map;

/**

* @author JavaEdge

* @date 2022/4/1

*/

@Getter

class Customer {

   private String name;

   private static Map<String, Customer> instances = new HashMap<>();

 

   private Customer(String name) {

       this.name = name;

   }

   public static Customer create(String name) {

       return new Customer(name);

   }

   static void loadCustomers() {

       new Customer("Java").store();

       new Customer("Edge").store();

       new Customer("公众号").store();

   }

   private void store() {

       instances.put(this.getName(), this);

   }

}



修改工厂函数,让它返回预先创建好的Customer对象


package com.javaedge.refactor.ttt;

import lombok.AllArgsConstructor;

import lombok.Getter;

import java.util.Dictionary;

import java.util.HashMap;

import java.util.Map;

/**

* @author JavaEdge

* @date 2022/4/1

*/

@Getter

class Customer {

   private String name;

   private static Map<String, Customer> instances = new HashMap<>();

   private Customer(String name) {

       this.name = name;

   }

   public static Customer create(String name) {

       return instances.get(name);

   }

   static void loadCustomers() {

       new Customer("Java").store();

       new Customer("Edge").store();

       new Customer("公众号").store();

   }

   private void store() {

       instances.put(this.getName(), this);

   }

}


由于create()总是返回既有的Customer对象,所以我应该使用Rename Method修改这个工厂函数的名称,以便强调这点:


public static Customer getNamed(String name) {

 return instances.get(name);

}


目录
相关文章
|
8月前
|
编译器 C#
C#.Net筑基-类型系统②常见类型 --record是什么类型?
`record`在C#中是一种创建简单、只读数据结构的方式,常用于轻量级数据传输。它本质上是类(默认)或结构体的快捷形式,包含自动生成的属性、`Equals`、`ToString`、解构赋值等方法。记录类型可以继承其他record或接口,但不继承普通类。支持使用`with`语句创建副本。例如,`public record User(string Name, int Age)`会被编译为包含属性、相等比较和`ToString()`等方法的类。记录类型提供了解构赋值和自定义实现,如密封的`sealed`记录,防止子类重写。
|
存储 安全 编译器
02-📝C++核心语法|C++对C的扩展【::作用域运算符、名字控制、struct类型加强、C/C++中的const、引用(reference)、函数】
复习`C++核心语法`,且适当进行汇编探索底层实现原理,进一步夯实基础,为以后的`底层开发`、`音视频开发`、`跨平台开发`、`算法`等方向的进一步学习埋下伏笔。
02-📝C++核心语法|C++对C的扩展【::作用域运算符、名字控制、struct类型加强、C/C++中的const、引用(reference)、函数】
|
存储 编译器 C#
《More effective C#》第二章 尽量采用隐式属性来表示可变的数据
《More effective C#》第二章 尽量采用隐式属性来表示可变的数据
index.js:39 错误:修饰类属性失败。请确保提案类属性已启用并设置为使用松散模式。要在规范模式下将提案类属性与修饰器一起使用,请在阶段 2 中等待下一个主要版本的装饰器。 #79
index.js:39 错误:修饰类属性失败。请确保提案类属性已启用并设置为使用松散模式。要在规范模式下将提案类属性与修饰器一起使用,请在阶段 2 中等待下一个主要版本的装饰器。 #79
88 0
|
Python
Python面向对象、类的抽象、类的定义、类名遵循大驼峰的命名规范创建对象、类外部添加和获取对象属性、类内部操作属性魔法方法__init__()__str__()__del__()__repr__()
面向对象和面向过程,是两种编程思想. 编程思想是指对待同一个问题,解决问题的套路方式.面向过程: 注重的过程,实现的细节.亲力亲为.面向对象: 关注的是结果, 偷懒.类和对象,是面向对象中非常重要的两个概念object 是所有的类基类,即最初始的类class 类名(object): 类中的代码PEP8代码规范:类定义的前后,需要两个空行 创建的对象地址值都不一样如dog和dog1的地址就不一样,dog的地址为2378043254528dog1的地址为2378044849840 8.类内部操作属性 sel
276 1
Python面向对象、类的抽象、类的定义、类名遵循大驼峰的命名规范创建对象、类外部添加和获取对象属性、类内部操作属性魔法方法__init__()__str__()__del__()__repr__()
重构——21将引用对象改为值对象(Change Reference to Value)
将引用对象改为值对象(Change Reference to Value):你有一个引用对象,很小且不可改变,而且不容易管理;将它变为一个值对象
1429 0
重构——20将值对象改为引用对象(Change Value to Reference)
将值对象改为引用对象(Change Value to Reference):你从一个类衍生出许多彼此相等的实例,希望将它们替换为同一对象;将这个值对象变成引用对象
1383 0