💎一站式轻松地调用各大LLM模型接口,支持GPT4、智谱、豆包、星火、月之暗面及文生图、文生视频 广告
反向重构:将引用对象改为值对象(252) ![](https://box.kancloud.cn/b3d3413afef724affcc34c762e1e35fb_427x374.jpeg) `let customer = new Customer(customerData);`![](https://box.kancloud.cn/a3bed334e2e1f6d1a46c5039deb25af9_91x152.jpeg) `let customer = customerRepository.get(customerData.id);`### 动机 一个数据结构中可能包含多个记录,而这些记录都关联到同一个逻辑数据结构。例如,我可能会读取一系列订单数据,其中有多条订单属于同一个顾客。遇到这样的共享关系时,既可以把顾客信息作为值对象来看待,也可以将其视为引用对象。如果将其视为值对象,那么每份订单数据中都会复制顾客的数据;而如果将其视为引用对象,对于一个顾客,就只有一份数据结构,会有多个订单与之关联。 如果顾客数据永远不修改,那么两种处理方式都合理。把同一份数据复制多次可能会造成一点困扰,但这种情况也很常见,不会造成太大问题。过多的数据复制有可能会造成内存占用的问题,但就跟所有性能问题一样,这种情况并不常见。 如果共享的数据需要更新,将其复制多份的做法就会遇到巨大的困难。此时我必须找到所有的副本,更新所有对象。只要漏掉一个副本没有更新,就会遭遇麻烦的数据不一致。这种情况下,可以考虑将多份数据副本变成单一的引用,这样对顾客数据的修改就会立即反映在该顾客的所有订单中。 把值对象改为引用对象会带来一个结果:对于一个客观实体,只有一个代表它的对象。这通常意味着我会需要某种形式的仓库,在仓库中可以找到所有这些实体对象。只为每个实体创建一次对象,以后始终从仓库中获取该对象。 ### 做法 - 为相关对象创建一个仓库(如果还没有这样一个仓库的话)。 - 确保构造函数有办法找到关联对象的正确实例。 - 修改宿主对象的构造函数,令其从仓库中获取关联对象。每次修改后执行测试。 ### 范例 我将从一个代表“订单”的`Order`类开始,其实例对象可从一个JSON文件创建。用来创建订单的数据中有一个顾客(customer)ID,我们用它来进一步创建`Customer`对象。 ##### class Order... ``` constructor(data) { this._number = data.number; this._customer = new Customer(data.customer); // load other data } get customer() {return this._customer;} ``` ##### class Customer... ``` constructor(id) { this._id = id; } get id() {return this._id;} ``` 以这种方式创建的`Customer`对象是值对象。如果有5个订单都属于ID为`123`的顾客,就会有5个各自独立的`Customer`对象。对其中一个所做的修改,不会反映在其他几个对象身上。如果我想增强`Customer`对象,例如从客户服务获取到了更多关于顾客的信息,我必须用同样的数据更新所有5个对象。重复的对象总是会让我紧张——用多个对象代表同一个实体(例如一名顾客),这会招致混乱。如果`Customer`对象是可变的,问题就更加严重,因为各个对象之间的数据可能不一致。 如果我想每次都使用同一个`Customer`对象,那么就需要有一个地方存储这个对象。每个应用程序中,存储实体的地方会各有不同,在最简单的情况下,我会使用一个仓库对象\[mf-repos\]。 ``` let _repositoryData; export function initialize() {  _repositoryData = {};  _repositoryData.customers = new Map(); } export function registerCustomer(id) {  if (! _repositoryData.customers.has(id))   _repositoryData.customers.set(id, new Customer(id));  return findCustomer(id); } export function findCustomer(id) {  return _repositoryData.customers.get(id); } ``` 仓库对象允许根据ID注册顾客,并且对于一个ID只会创建一个`Customer`对象。有了仓库对象,我就可以修改`Order`对象的构造函数来使用它。 在使用本重构手法时,可能仓库对象已经存在了,那么就可以直接使用它。 下一步是要弄清楚,`Order`的构造函数如何获得正确的`Customer`对象。在这个例子里,这一步很简单,因为输入数据流中已经包含了顾客的ID。 ##### class Order... ``` constructor(data) {  this._number = data.number;  this._customer = registerCustomer(data.customer);  // load other data } get customer() {return this._customer;} ``` 现在,如果我在一条订单中修改了顾客信息,就会同步反映在该顾客拥有的所有订单中。 在这个例子里,我在第一个引用该顾客信息的`Order`对象中新建了`Customer`对象。另一个常见的做法是:首先获取一份包含所有`Customer`对象的列表,将其填入仓库对象,然后在读取`Order`对象时关联到对应的`Customer`对象。如果这样做,那么`Order`对象包含的顾客ID必须指向一个仓库中已有的`Customer`对象,否则就表示程序中有错误。 上面的代码还有一个问题:构造函数与一个全局的仓库对象耦合。全局对象必须小心对待:它们就像强力的药物,少用一点儿大有益处,用过量就是毒药。如果想解决这个问题,可以将仓库对象作为参数传递给构造函数。