《重构》读书笔记

《重构》学习笔记(06)-- 重新组织数据

2019-07-13  本文已影响0人  若隐爱读书

在面向对象的语言中,通常会有直接访问数据还是通过方法访问数据的争论。同时,面向对象的语言也允许使用自己定义的新类型取代传统语言的简单数据类型。将数组转换为对象、自封装字段魔法数字的消除,都是本周要介绍的点。

Self encapsulate Field(自封装字段)

你可以直接访问一个字段,但是字段之间的耦合关系会逐渐变得笨拙。因此为字段设置set/get方法,并且只以这些方法来访问字段。就称为自封装字段。

public class RefactorMain {
    private int _low, _high;

    boolean includes(int arg) {
        return (arg >= _low && arg <= _high);
    }
}

重构为

public class RefactorMain {
    //重构之后
    private int _low, _high;

    boolean includes(int arg) {
        return (arg >= getLow() && arg <= getHigh());
    }

    int getLow() {
        return _low;
    }

    int getHigh() {
        return _high;
    }
}

在“字段访问方式”这个问题上,存在两种截然不同的观点。总结优缺点如下:间接访问变量的好处是,子类可以通过覆写一个函数而改变获取数据的路径,它还支持更灵活的数据管理方式,例如懒加载。直接访问的方式的好处是代码比较容易阅读,阅读代码时不用查看函数定义才知道用法。通常重构做法:

Replace Data Value with Object(以对象取代数据值)

如有有一个数据项,需要与其他数据和行为放在一起使用才有意义,那么将其变成对象

重构前
重构后
重构后
随着程序的开发,一些原本简单的字符串,可能与其他数据适合组装成为一个对象。通常的重构做法为:

Change Value to Reference(将值对象改为引用对象)

本节理解欠佳,需重复阅读。

你有一个class,衍生出许多相等实体(equal instances),你希望将它们替换为单一对象。将这个value object(实值对象)变成一个reference object(引用对象)。

我们举一个例子。设计一个顾客与订单的系统,在这个系统中,一个订单对应一个顾客,但是多个订单可能是一个顾客产生的。原代码如下:

class Customer {
    public Customer(String name) {
       _name = name;
    }

    public String getName() {
       return _name;
    }
    private final String _name;
}

它被以下的order class使用:

class Order...
    public Order(String customerName) {
       _customer = new Customer(customer);
    }

    public String getCustomerName() {
       return _customer.getName();
    }
    
    public void setCustomer(String customerName) {
       _customer = new Customer(customerName);
    }
    private Customer _customer;

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

private static int numberOfOrdersFor(Collection orders, String customer) {
    int result = 0;
    Iterator iter = orders.iterator();
    while(iter.hasNext()) {
       Order each = (Order)iter.next();
       if(each.getCustomerName().equals(customer)) result ++;
    }
    return result;
}

这种设计中,即使多份订单同属于一个客户,但是每个Order对象还是拥有各自的Customer对象。

我们对这段代码进行重构,为简单起见,我们在Customer中新建一个static字段模拟静态字典。

class Customer...
    static void loadCustomers() {
       new Customer("Lemon Car Hire").store();
        new Customer("Associated Coffee Machines").store();
        new Customer("Bilston Gasworks").store();
    }
    private void store() {
       _instance.put(this.getName(), this);
    }

现在,我要修改factory method,让它返回预先创建好的Customer对象:

public static Customer create(String name) {
    return (Customer)_instance.get(name);
}

由于create()总是返回既有的Customer对象,所以我应该使用Rename Method(273)修改这个factory method的名称,以便强调(说明)这一点。

class Customer...
public static Customer getNamed(String name) {
    return (Customer)_instances.get(name);
}

总结下,这种重构通常的做法为:

Change Reference to Value(将引用对象改为值对象)

值对象应该是不可变的。无论何时,调用此对象的查询函数得到的都是一个结果,比如第一个例子中,customer作为值对象,每个order 都有自己的一份customer。

order1.getCustomer("张三").setTelepho("123");
order2.getCustomer("张三").getTelepho();

如果张三开始的号码是135。order2得到的customer 的值还是135,虽然order1已经改变了张三的电话号码。
引用对象应该是可变的,确保某一对象修改,自动会更新其它代表某一相同事物的其它对象的修改。要把reference Object 变成value Object 只需要重写equals()和hashCode()两个方法,并且去掉Method Factory 对构造函数的调用。通常用的做法为:

Replace Array with Object(以对象取代数组)

如果你有一个数组,但是数组中并没有排列的关系,那么以对象替换数组,对于数组中的每个元素,以一个字段来表示。

String[] row = new String[3];
row[0] = "Livepool";
row[1] = "15";

重构后

performance row = new Performance();
row.setName("Livepool");
row.setWins("15");

使用这种重构手段,可以用变量名去自注释。这种重构方法,应该重视调用地方不要漏改。通常的做法为:

Duplicate Observed Data(复制“被监视数据”)

一个设计良好的系统,view层和业务逻辑应该分开。一方面业务层可能支撑不同的view层,另一方面有利于模块解耦。由于前端框架大部分都考虑了MV分离,因此本节不再详细描述。有需要的同学可以购买《重构》这本书了解。
这里描述下duplicate Obeserved Data的通常做法:
做法

Change Unidirectional Association to Bidirectional(将单向关联改为双向关联)

如果两个类都需要用到对方的特性,但其间只有一条单向链接。这时候就需要加一条"反向指针"。不过笔者以为双向关联会增加系统的复杂度,不符合现代软件“依赖倒置”原则。除非非常有必要,否则不要使用双向关联。
单向关联改双向关联的通常做法为:

class Order {
  getCustomer() {
    return this._customer
  } 
  setCustomer(arg) {
    this._customer = arg
  }
}

重构后

class Customer {
  _orders = new Set()
 
  friendOrders() {
    return this._orders
  }
 
  addOrder(arg) {
    arg.setCustomer(this)
  }
}
 
class Order {
  getCustomer() {
    return this._customer
  }
 
  /**
   * 控制函数
   * @param {} arg 
   */
  setCustomer(arg) {
    if(arg) {
      this._customer.friendOrders().delete(this)
    }
    this._customer = arg
    if(this._customer) {
      this._customer.friendOrders().add(this)
    }
  }
}

以上例子中,Order新增加了一个控制函数进行对Customer的控制。通常,一对多的系统由单一方承担控制者角色。如果多对多,那么无所谓。

Change Bidirectional Association to Unidirectional(将双向关联改为单向关联)

双向关联的弊端在于要维护双向连接、确保对象被正确的创建和删除而增加复杂度,并且大量的双向连接容易造成"僵尸对象"。只有真正需要双向关联的时候才去使用它,否则就去掉其中一条关联。
改为单向关联的通常做法为:

class Customer {
  _orders = new Set()
 
  friendOrders() {
    return this._orders
  }
 
  addOrder(arg) {
    arg.setCustomer(this)
  }
 
  getPriceFor(order) {
    return order.getDiscountedPrice()
  }
}
 
class Order {
  getCustomer() {
    return this._customer
  }
 
  /**
   * 控制函数
   * @param {} arg 
   */
  setCustomer(arg) {
    if(arg) {
      this._customer.friendOrders().delete(this)
    }
    this._customer = arg
    if(this._customer) {
      this._customer.friendOrders().add(this)
    }
  }
 
  getDiscountedPrice() {
    return this.getGrossPrice() * (1- this._customer.getDiscount())
  }
}

重构为

class Customer {
  _orders = new Set()
 
  friendOrders() {
    return this._orders
  }
 
  addOrder(arg) {
    arg.setCustomer(this)
  }
 
  getPriceFor(order) {
    return order.getDiscountedPrice(this)
  }
}
 
class Order {
  getDiscountedPrice(customer) {
    return this.getGrossPrice() * (1- customer.getDiscount())
  }
}

Replace Magic Number with Symbolic Constant(以字面常量取代魔法数)

代码中的魔法数字是最悠久的不良现象之一,它的缺点在于无法自注释,而且多个地点引用同一逻辑数,不符合开闭原则。

mass * 9.8 * height

可以重构为

static final double GRAVITATIONAL = 9.8;
...
mass * GRAVITATIONAL * height;

注意:通常常量要大写。
这种重构的做法为:

Encapsulate Field(封装字段)

这种在Java中毕竟常见,将一个public 字段 增加set/get方法,并将自己修改为private,达到“数据隐藏”的效果。例如

private String _name;
public String getName(){
    return _name;
}
public void setName(String name){
    _name = name;
}

这种重构毕竟简单,为了规范化也写上常用的步骤。

Encapsulate Collection(封装集合)

如果类中包含一个集合。那么取值函数不应该返回集合自身,因为这会让用户得以修改集合内容而集合拥有者却一无所知。不应该为整个集合提供一个设值函数,但应该提供用以为集合添加/移除元素的函数。这样,集合拥有者(对象)就可以控制集合元素的添加和移除。
举个例子:

class Course {
  constructor(name, isAdvanced) {
    this._name = name
    this._isAdvanced = isAdvanced
  }
 
  isAdvanced() {
    return this._isAdvanced
  }
}
 
class Person {
  getCourses() {
    return this._courses
  }
 
  setCourses(arg) {
    this._courses = arg
  }
}

重构为:

class Course {
  constructor(name, isAdvanced) {
    this._name = name
    this._isAdvanced = isAdvanced
  }
 
  isAdvanced() {
    return this._isAdvanced
  }
}
 
class Person {
  constructor() {
    this._courses = []
  }
 
  addCourse(arg) {
    return this._courses.push(arg)
  }
 
  removeCourse(arg) {
    this._courses.filter(item => item !== arg)
  }
 
  initializeCourses(arg) {
    this._courses = this._courses.concat(arg)
  }
 
  getCourses() {
    return this._courses.map(item => item)
  }
}

思想就是隐藏和封装

Replace Record with Data Class(以数据类取代记录)

在前端中遇到较少,暂不做笔记

Replace Type Code with Subclass(以子类取代类型码)

在前端中遇到较少,暂不做笔记

Replace Type Code with State/Strategy(以State/Strategy取代类型码)

在前端中遇到较少,暂不做笔记

Replace Subclass with Fields(以字段取代子类)

如果各子类中只有“常量函数”,那么就可以将子类去除,只保留超类。例如以下结构


重构前

可以重构为


重构后
这种重构常用的方法为:

本章所述部分方法互为镜像,通常需要开发者结合代码总体情况采用不同的重构手段进行重构。在重构过程中,要时刻牢记代码重构原则:【单一职责】【里氏替换】【迪米特法则】【依赖倒置原则】【接口隔离原则】【开闭原则】。

上一篇 下一篇

猜你喜欢

热点阅读