0. 本章内容导图
本章介绍的是如何更好地处理数据的重构手法。
重新组织数据
1. 重构手法
1.1 自封装字段
概要:
你直接访问一个字段,但与字段之间的耦合关系逐渐变得笨拙。
为这个字段建立取值/设值函数,并且只以这些函数来访问字段。
动机:
a. 方便管理字段,可以对字段进行统一控制与处理
示例:
重构前:
private int mLow;
private int mHigh;
boolean includes(int arg) {
return arg >= mLow && arg <= mHigh;
}
重构后:
private int mLow;
private int mHigh;
int getLow() {
return mLow;
}
int getHigh() {
return mHigh;
}
boolean includes(int arg) {
return arg >= getLow() && arg <= getHigh();
}
总结:
直接访问字段简明直接,间接访问字段方便对字段进行统一的处理,使用哪种方式需依据实际的需求而定。可先按直接访问方式,有需要时再用本重构手法将其改为间接方式。
1.2 以对象取代数据值
概要:
你有一个数据项,需要与其他数据和行为一起使用才有意义。
将数据项变成对象。
动机:
a. 将数据和对数据的行为封装在一起
示例:
重构前:
class Order {
//以一个String声明一个客户
private String mCustomer;
public Order(String customer) {
mCustomer = customer;
}
public String getCustomer() {
return mCustomer;
}
public void setCustomer(String customer) {
mCustomer = customer;
}
}
重构后:
class Customer {
private final String mName;
public Customer(String name) {
mName = name;
}
public String getName() {
return mName;
}
}
class Order {
private Customer mCustomer;
public Order(String customer) {
mCustomer = new Customer(customer);
}
public String getCustomer() {
return mCustomer.getName();
}
public void setCustomer(String customer) {
mCustomer = new Customer(customer);
}
}
总结:
数据和行为分离是面向过程的思考方式,要以面向对象的方式思考问题,利用封装,将数据和对数据的操作封装在一起。
1.3 将值对象改为引用对象
概要:
你从一个类衍生出许多彼此相等的实例,希望将它们替换为同一个对象。
将这个值对象变成引用对象。
动机:
a. 将一对一依赖改为多对一依赖
示例图:
值对象改为引用对象
总结:
有时候,你会发现系统中创建了很多个相同的对象(这些对象的内容相同),比如每个订单对象都对应一个特定的客户对象,十个订单对象就对应十个客户对象,而这十个客户对象可能就是同一个客户,你只需要让十个订单对象对应同一个客户对象就行了。
1.4 将引用对象改为值对象
概要:
你有一个引用对象,很小且不可变,而且不易管理。
将它变成一个值对象。
动机:
a. 将多对一依赖改为一对一依赖
示例图:
引用对象改为值对象
总结:
前提是这个对象是不可变的,“不可变”意味着如果你要改变这个对象的内容,你必须重新创建一个对象。
1.5 以对象取代数组
概要:
你有一个数组,其中的元素各自代表不同的东西。
以对象替换数组。对于数组中的每个元素,以一个字段来表示。
动机:
a. 消除数组元素表征意义不一致造成的困扰
示例:
重构前:
String[] row = new String[3];
row[0] = "Liverpool";
row[1] = "15";
重构后:
Performance row = new Performance();
row.setName("Liverpool");
row.setWins("15");
总结:
数组容纳的对象不仅类型相同,所表征的意义也应该是相同的,否则,就会给使用者造成误解。
1.6 复制“被监视数据”
概要:
你有一些领域数据置身于GUI控件中,而领域函数需要访问这些数据。
将该数据复制到一个领域对象中。建立一个Observer模式,用以同步领域对象和GUI对象内的重复数据。
动机:
a. 建立良好的分层,分离表现层和逻辑层
示例图:
演化出Observer模式
总结:
很多时候代码一开始层次划分的未必清晰,如早期的Android apk开发,很多都是将业务逻辑代码同负责界面显示的Activity代码混在一起,导致Activity逐渐演变成为“上帝类”。在包含GUI的系统内,表现层和逻辑层都要依赖数据层,这些数据往往还需要同步,Observer模式正是定义了对象之间的这种一对多的关系。
Observer模式:定义对象间的一种一对多的依赖关系。当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并被自动更新。
1.7 将单向关联改为双向关联
概要:
两个类都需要使用对方特性,但其间只有一条单向连接。
添加一个反向指针,并使修改函数能够同时更新两条连接。
动机:
a. 方便类之间互相引用
示例图:
单向关联改为双向关联
总结:
双向关联造成类的相互依赖,如非必要,尽量不要使用。系统中存在过多的双向关联会让系统变得混乱不堪。可以引入一个中介者来处理这种相互依赖。
1.8 将双向关联改为单向关联
概要:
两个类之间有双向关联,但其中一个类如今不再需要另一个类的特性。
去除不必要的关联。
动机:
a. 去除不必要的双向关联,消除双向关联带来的各种副作用
示例图:
双向关联改为单向关联
总结:
相较于单向关联,维护双向关联需要更大的代价,复杂度也更高;双向关联也容易造成“僵尸对象”,使得一些本该回收的垃圾对象因仍存在引用而无法回收;双向关联使得两个类相互依赖,对其中任一个类的修改都可能会影响到另一个类,这种紧耦合会使得系统变得不稳定。
1.9 以字面常量取代魔法数
概要:
你有一个字面数值,带有特别含义。
创造一个常量,根据其意义为它命名,并将上述的字面数值替换为这个常量。
动机:
a. 明确魔法数的意义
b. 方便修改
示例:
重构前:
double potentialEnergy(double mass, double height) {
return mass * 9.81 * height;
}
重构后:
static final double GRAVITATIONAL_CONSTANT = 9,81;
double potentialEnergy(double mass, double height) {
return mass * GRAVITATIONAL_CONSTANT * height;
}
总结:
用字面常量替换魔法数,不仅大大提高代码的可读性,在需要修改时,也只用在定义处修改就可以了。
1.10 封装字段
概要:
你的类中存在一个public字段。
将它声明为private,并提供相应的访问函数。
动机:
a. 将数据和操作数据的行为封装在一个类中
示例:
重构前:
public String mName;
重构后:
private String mName;
public String getName() {
return mName;
}
public void setName(String name) {
mName = name;
}
总结:
字段声明为public,其他对象可以随意修改这个数据,会使得对象的状态失去控制。再者,通过访问函数访问数据,还可在访问函数中提供统一的控制。
1.11 封装集合
概要:
有个函数返回一个集合。
让这个函数返回该集合的一个只读副本,并在这个类中提供添加/移除集合元素的函数。
动机:
a. 避免对用户暴露过多对象内部的数据结构信息
b. 避免用户在集合所属对象不知悉的情况下随意修改集合内容
示例:
重构前:
// Course表示要上的课程,advanced表示高级课程
class Course {
public Course(String name, boolean isAdvanced) {...}
public boolean isAdvanced() {...}
}
class Person {
private Set mCourses;
public Set getCourses() {
return mCourses;
}
public void setCourses(Set courses) {
mCourses = courses;
}
}
/*************外部使用***************/
Person paul = new Person();
Set s = new HashSet();
s.add(new Course("疯狂Java讲义", false));
s.add(new Course("Java编程思想", true));
paul.setCourses(s);
//添加新课程
Course newCourse = new Course("Java与模式", true);
paul.getCourses().add();
//删除课程
paul.getCourses().remove(newCourse);
重构后:
class Person {
private Set mCourses = new HashSet();
//对外提供添加功能,使用者无需知晓内部实现细节
public void addCourse(Course course) {
mCourses.add(course);
}
//对外提供删除功能,使用者无需知晓内部实现细节
public void removeCourse(Course course) {
mCourses.remove(course);
}
//返回集合的只读副本
public Set getCourses() {
return Collections.unmodifiableSet(mCourses);
}
}
/*************外部使用***************/
Person paul = new Person();
paul.addCourse(new Course("疯狂Java讲义", false));
paul.addCourse(new Course("Java编程思想", true));
//添加新课程
Course newCourse = new Course("Java与模式", true);
paul.addCourse(newCourse);
//删除课程
paul.removeCourse(newCourse);
总结:
返回集合自身是很危险的,外界可以随意修改集合的内容。要明确对象的接口功能,提供外界所需的功能接口,隐藏功能的实现细节,这样当接口内部变更时才不至于影响到客户代码。
1.12 以数据类取代记录
概要:
你需要面对传统编程环境中的记录结构。
为该记录创建一个“哑”数据对象。
动机:
a. 将记录结构对象化
总结:
在需要对接遗留代码或者处理从数据库读出记录时,需要创建这么个数据类,提供data<->object的功能。数据对象中还可添加更多与数据操作相关的行为。
1.13 以类取代类型码
概要:
类之中有一个数值类型码,但它并不影响类的行为。
以一个新的类替换该数值类型码。
动机:
a. 用类取代类型码,使编译器可以对类进行类型检测
示例:
重构前:
class Person {
public static final int O = 0;
public static final int A = 1;
public static final int B = 2;
public static final int AB = 3;
private int mBloodGroup;
public Person(int bloodGroup) {
mBloodGroup = bloodGroup;
}
public void setBloodGroup(int bloodGroup) {
mBloodGroup = bloodGroup;
}
public void getBloodGroup() {
return mBloodGroup;
}
}
重构后:
class BloodGroup {
public static final BloodGroup O = new BloodGroup(0);
public static final BloodGroup A = new BloodGroup(1);
public static final BloodGroup B = new BloodGroup(2);
public static final BloodGroup AB = new BloodGroup(3);
private final int mCode;
private BloodGroup(int code) {
mCode = code;
}
}
class Person {
private BloodGroup mBloodGroup;
public Person(BloodGroup bloodGroup) {
mBloodGroup = bloodGroup;
}
public BloodGroup getBloodGroup() {
return mBloodGroup;
}
public void setBloodGroup(BloodGroup bloodGroup) {
mBloodGroup = bloodGroup;
}
}
总结:
只有类型码是纯粹数据,且不会影响类的行为发生变化时,才可以用类来取代它。
1.14 以子类取代类型码
概要:
你有一个不可变的类型码,它会影响类的行为。
以子类取代这个类型码。
动机:
a. 以类型码的宿主类为基类,针对每种类型码建立相应的子类,构建继承体系
示例图:
以子类取代类型码
总结:
本重构手法主要作用是搭建一个继承体系,使得可以运用“以多态取代条件表达式”得以顺利开展。构建起继承体系后,如果需要加入新的行为变化,只需要添加一个子类就行了,符合开闭原则。
有两种情况不能运用本项重构手法:
a) 类型码在对象创建之后会发生改变
b) 类型码宿主类已经有子类了
如遇这两种情况,就需要运用“以State/Strategy取代类型码”这种重构手法
1.15 以State/Strategy取代类型码
概要:
你有一个类型码,它会影响类的行为,但你无法通过继承手法消除它。
以状态对象取代类型码。
动机:
a. 构建一个继承体系取代类型码,使原宿主类依赖新构建的这个继承体系
示例图:
以State/Strategy取代类型码
总结:
以状态对象取代类型码仅是一种中间状态,当通过状态对象取代类型码后,后续继续“以多态取代条件表达式”,一步步重构出State模式或Strategy模式。
以类取代类型码、以子类取代类型码、以State/Strategy取代类型码比较:
a. 以类取代类型码:类型码是数值型的,它们不会影响类的行为。
b. 以子类取代类型码:类型码的值在对象生命周期中不会发生变化,不同的类型码对象的行为不同。
c. 以State/Strategy取代类型码:类型码的值在对象生命周期中会发生变化,不同的类型码对象的行为不同。
d. 以子类取代类型码依靠继承达成目的,以State/Strategy取代类型码依靠组合和继承达成目的。
1.16 以字段取代子类
概要:
你的各个子类的唯一差别只在“返回常量数据”的函数身上。
修改这些函数,使它们返回超类中的某个(新增)字段,然后销毁子类。
动机:
a. 消除没有价值的子类
示例:
重构前:
abstract class Person {
abstract boolean isMale();
abstract char getCode();
}
class Male extends Person {
boolean isMale() {
return true;
}
char getCode() {
return 'M';
}
}
class Female extends Person {
boolean isMale() {
return false;
}
char getCode() {
return 'F';
}
}
重构后:
class Person {
private final boolean mIsMale;
private final char mCode;
Person(boolean isMale, char code) {
mIsMale = isMale;
mCode = code;
}
public static Person createMale() {
return new Person(true, 'M');
}
public static Person createFemale() {
return new Person(false, 'F');
}
public boolean isMale() {
return mIsMale;
}
public char getCode() {
return mCode;
}
}
总结:
建立子类的目的,是为了增加父类没有的新特性或改变父类的某些行为。有一种改变行为的方法被称为“常量函数”,常量函数会返回一个硬编码的值,不同的子类会返回不同的硬编码值。常量函数有其自身的用途,但若是子类中只有常量函数,就没有足够的存在价值。去除这样的子类,也可以避免因继承带来的额外复杂性,减少维护的成本。








网友评论