美文网首页
重新组织数据

重新组织数据

作者: 塞外的风 | 来源:发表于2018-06-22 11:22 被阅读0次

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;
    }
}

总结:
    建立子类的目的,是为了增加父类没有的新特性或改变父类的某些行为。有一种改变行为的方法被称为“常量函数”,常量函数会返回一个硬编码的值,不同的子类会返回不同的硬编码值。常量函数有其自身的用途,但若是子类中只有常量函数,就没有足够的存在价值。去除这样的子类,也可以避免因继承带来的额外复杂性,减少维护的成本。

相关文章

  • 重新组织数据

    0. 本章内容导图 本章介绍的是如何更好地处理数据的重构手法。 1. 重构手法 1.1 自封装字段 概要:你直接访...

  • 重新组织数据

    重新组织数据 Self Encapsulate Field对于属性的访问采用访问函数的形式。通常这不是必须的,我们...

  • 《重构》- 重新组织数据

    一. Self Encapsulate Field(自封装字段) 介绍 场景你直接访问一个字段,但与字段之间的耦合...

  • 重构——重新组织数据

    1 Self Encapsulate Field(自封装字段) 直接访问一个字段,会导致字段之间的的耦合关系过于笨...

  • 容器类型-列表和元祖

    一 概要 二.数据收纳盒 三 列表和元祖的创建 四 列表重新组织

  • 《重构》读书笔记 第八章

    重新组织数据 8.1 Self Encapsulate Field (自封装字段) 为字段建立取值/设值函数,并且...

  • 重构手法--重新组织数据

    1、自封装值域 在对象内部,不直接访问自己的私有属性,而通过get/set方法访问 2、以对象取代数据值 如果某个...

  • 003-重新组织数据

    重新组织数据 1. Self Encapsulate Field(自封装字段) Q:你直接访问一个字段,但与字段之...

  • 代码重构之重新组织数据

    1.引言 最近看代码重构的数据遇到问题了。很多重构手法都不是很明白,于是捡自己遇到的能理解的,记录下。 2.正题 ...

  • 第8章 重新组织数据

    目录 SelfEncapsulateField(自封装字段) ReplaceDataValuewithObject...

网友评论

      本文标题:重新组织数据

      本文链接:https://www.haomeiwen.com/subject/nxstyftx.html