程序员社区

重构读书笔记


title: 重构读书笔记
date: 2019/11/05


重构:对软件内部结构的一种调整,目的是在不改变软件可观察行为的前提下提高其可理解性降低其修改成本

重构的前提是单元测试。

六、重新组织函数

6.1 提炼函数

你有一段代码可以被组织在一起并独立出来 => 将这段代码放进一个独立的函数中,并让函数名称解释该函数的用途

void printOwing(double amount) {
    printBanner();

    sout("name:" + _name);
    sout("amount" + _amount);
}

=>

void printOwing(double amount) {
    printBanner();

    printDetails();
}

void printDetails() {
    sout("name:" + _name);
    sout("amount" + _amount);
}

何时使用?

  1. 过长的函数
  2. 需要注释才能让人理解用途的代码

好处?

  1. 函数的粒度很小,函数复用的机会就会更大
  2. 如果函数命名良好,就会使高层函数读起来就像一系列注释

一个函数多长合适?

长度不是问题,关键在于函数名称和函数本体之间的语义距离。

做法?

  1. 创建一个新函数,根据这个函数的意图为他起一个合适的名字(以“做什么”而不是“怎样做”来命名)。

即使想要提炼的代码非常简单,例如只是一条消息或一个函数调用,只要新函数的名称能以更好的表达代码的意图,就应该提炼他。

如果想不出一个更有意义的名称,就别动

  1. 仔细检查“提炼代码段”,看看是否引用了“作用域仅在源函数”的变量(包括局部变量和源函数参数)。

  2. 检查是否有任何仅用于“提炼代码段”的局部变量。如果有,在“提炼代码段”声明为局部变量。

  3. 检查“提炼代码段”,看看是否有任何局部变量被他改变。如果一个临时变量值被修改了,看看是否可以将“提炼代码段”处理为一个查询,并将结果赋给相关变量。

  4. 传入“提炼代码段”需要的局部变量。

如果需要返回多个变量怎么办?

分为多个函数,每个返回一个变量。

6.2 内联函数

一个函数的本体与名称一样通俗易懂,而且比较简单 => 将函数中的代码copy到调用函数中,删除该函数

int getRating() {
    return (moreThanFiveLateDeliveries) ? 2 : 1;
}

boolean moreThanFiveLateDeliveries() {
    return _numberOfLateDeliveries > 5;
}

=> 

int getRating() {
    return (_numberOfLateDeliveries > 5) ? 2 : 1;
}

此重构方法同样适用于,使用了太多间接层的代码。

6.3 内联临时变量

一个临时变量,只被一个简单的表达式赋值了一次,而它妨碍了其它重构方法 => 将所有对改变了的引用动作,替换为对它赋值的那个表达式自身。

double basePrice = anOrder.basePrice();
return basePrice > 1000;

=>

return (anOrder.basePrice()) > 1000;

6.4 以查询取代临时变量

以一个临时变量保存某一表达式的运算结果 => 将这个表达式提炼到一个独立函数中。将这个临时变量的的所有引用点替换为对新函数的引用。

double basePrice = _quantity * _itemPrice;

if (basePrice > 1000) {
    ...
}

=>

if (getBasePrice() > 1000) {
    ...
}

double getBasePrice() {
    return _quantity * _itemPrice;
}

其实我觉得复杂且可重用的表达式,可以提取出来,但是简单的就没必要了。

6.5 引入解释变量

一个复杂的表达式 => 将该复杂的表达式(或其中一部分)的结果放进一个临时变量,以此变量名解释表达式用途

if ((platform.toUpperCase().indexOf("MAC")) > -1) && (browser.toUpperCase().indexOf("IE") > -1) && wasInit() && resize > 0) {
    ...
}

final boolean isMacOS = platform.toUpperCase().indexOf("MAC") > -1;
final boolean isIE = browser.toUpperCase().indexOf("IE") > -1
final boolean wasResized = resize > 0

if (isMacOS && isIE && wasResized && wasInit()) {
    ...
}

如果这个复杂的算法其它方法也能用,就独立成方法,如果只有本方法再用,采用局部变量

6.6 分解临时变量

一个临时变量被赋值超过一次(他不是循环变量,也不用于收集计算结果) => 针对每次赋值,创建一个独立对应的临时变量

double temp = 2 * (_height + _width);
sout(temp);
temp = _height * _width
sout(temp);

=>

double 周长 = 2 * (_height + _width);
sout(周长);
double 面积 = _height * _width
sout(面积);

单一职责

6.7 移除对参数的赋值

代码对一个参数进行赋值 => 以临时变量取代参数的位置

降低的代码的清晰度。

6.8 以函数对象取代函数

一个大型函数,其中有很多局部变量,导致无法使用【提取方法】重构手段 => 将这个函数放入一个单独的对象中,如此一来局部变量就成为了对象中的字段,然后就可以在这个类中,把大型函数分解为多个小型函数

7、在对象间搬移特性

7.1 搬移函数

程序中,有个函数与其所驻类之外的另一个类进行更多交流(调用后者,或被后者调用) => 在另一个类中建立一个有着类似行为的新函数。将旧函数变成一个单纯的委托函数,或者直接把旧函数删除。

7.2 搬移字段

某个字段被其所驻类之外的另一个类更多的用到 => 在目标类新建一个字段,修改源字段的所有用户,令他使用新字段

7.3 提炼类

某个类做了两个类做的事 => 建立一个新类,将相关字段和函数移到新类

一个类应该是一个清楚的抽象,处理一些明确的责任。

但是在工作中,类会不断的成长扩展。不断的为它添加责任,从而导致这个类过于复杂。

这样的类往往有大量的函数和数据;例如Person类,其中有大量的和电话号码相关的字段(区号、电话、手机等),那么就可以将其移除出去,让他和Person类建立关系。

7.4 将类内联化

某个类没有做太多事情 => 将这个类的所有特性搬移到另一个类中,然后移除原类

如果一个类不再承担足够的责任,我们就可以使用这一重构手法。

7.5 隐藏委托关系

客户通过委托类来调用另一个对象 => 在服务类上建立客户所需的所有函数,用以隐藏委托关系

class Person {

    Department _department;

    public Department get_department() {
        return _department;
    }

    public void set_department(Department _department) {
        this._department = _department;
    }
}

// 部门
class Department {
    
    private String _chargeCode;
    
    private Person _manager;

    public String get_chargeCode() {
        return _chargeCode;
    }

    public void set_chargeCode(String _chargeCode) {
        this._chargeCode = _chargeCode;
    }

    public Person get_manager() {
        return _manager;
    }

    public void set_manager(Person _manager) {
        this._manager = _manager;
    }
}

// 客户想知道部门经理是谁;这种写法就向用户揭露了Department的工作原理
person.get_department().get_manager();

=>

class Person {
    Department _department;
    
    public Person get_manager() {
        return _department.get_manager();
    }

    ...

7.6 移除中间人

某个类做了过多的简单委托 => 让客户直接调用受托类

以上面的例子来说,如果Department类的方法全部都需要Person类进行委托,那么就应该让客户端直接调用Department类。

7.7 引入外加函数

要为提供服务的类增加一个函数,但无法修改服务类 => 在客户类中新建一个函数,并把服务类实例作为第一个参数传入。

Date newStart = new Date(previousEnds.getYear(), previousEnds.getMonth(), previousEnds.getDate() + 1);

=>

Date newStart = nextDay(previousEnds);

private static Date nextDay(Date date) {
    return new Date(date.getYear(), date.getMonth(), date.getDate() + 1);
}

7.8 引入本地扩展

要为提供服务的类增加一个函数,但无法修改服务类 => 建立一个新类,是它包含这些额外的函数,让这个扩展品成为源类的子类或包装类

8、重新组织数据

8.1 自封装字段

直接访问一个字段,但与字段之间的耦合关系变得笨拙 => 为这个字段建立get/set方法,并且只以这些函数来访问字段。

private int _low, _hight;

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

=>

private int _low, _hight;

boolean includes(int arg) {
    return arg >= get_low() && arg <= get_hight();
}

get/set方法略

8.2 以对象取代数据值

有一个数据项,必须要与其它数据和行为一起使用才有意义 => 将数据项变成对象。

开发初期,你往往决定以简单的数据项表示简单的情况。但是,随着开发的进行,你可能会发现,这些简单数据项不再那么简单了。

比如说,一开始你可能会用一个字符串来表示“电话号码”概念,但是随后你就会发现,电话号码需要“格式化”、“抽取区号”之类的特殊行为。当这种字段越来越多的时候,你就应该将数据值变成对象。

8.7 将单项关联改为双向关联

两个类都需要使用对方特性,但其中只有一条单向连接 => 添加一个反向指针,并使修改函数能够同时更新两条连接

8.9 以字面常量取代魔法数

一个字面数值,代表特殊含义 => 创造一个常量,根据其意义为他命名

double potentialEnergy(double mass, double height) {
    return mass * 9.8 * height;
}

=>

static final double 重力加速度 = 9.8;

double potentialEnergy(double mass, double height) {
    return mass * 重力加速度 * height;
}

8.11 封装集合

函数返回一个集合 => 函数返回一个只读副本,并在该类中提供添加/移除集合中元素的函数

9、简化条件表达式

9.1 分解条件表达式

有一个复杂的条件语句 => 从if、then、else三个段落分别提炼出独立函数。

if (date.before(SUMMER_START) || date.after(SUMMER_END)) {
    charge = quantity * _winterRate + _winterServiceCharge;
} else {
    charge = quantity + _summerRate;
}

=>

if (notSummer(date)) {
    charge = winterCharge(quantity);
} else {
    charge = summerCharge(quantity);
}

9.2 合并条件表达式

有一系列条件测试,都得到相同结果 => 将其合并成为一个条件表达式,并提炼为一个函数。

double disabilityAmount() {
    if (_seniority < 2) return 0;
    if (_monthsDisableed > 12) return 0;
    if (_isPartTime) return 0;
}

=>

double disabilityAmount() {
    if (this.isNotEligible4Disability()) return 0;
}

好处:

合并后的条件代码会告诉你,“实际只有一次条件检查,只不过有多个并列条件需要检查而已”。

将检查条件独立成为一个函数对应代码阅读很有好处,因为他把描述“做什么”的语句换成了”为啥这样做“

9.3 合并重复的条件片段

在条件表达式的每一个分支上有着一段相同的代码 => 将这段相同的代码移到条件表达式之外

if (flag) {
    total = price * 0.95;
    send();
} else {
    total = price * 0.98;
    send();
}

=>

if (flag) {
    total = price * 0.95;
} else {
    total = price * 0.98;
}
send();

9.4 移除控制标记

boolean flag = true;

while (flag) {
    if (doSomething()) {
        flag = false;
    }
}

=>

while (true) {
    if (doSomething()) {
        break;
    }
}

9.5 以卫语句取代嵌套条件表达式

result = xxx;

if (xxx) {
    if (yyy) {
        result = yyy;
    } else {
        result = zzz;
    }
} else {
    result = www;
}

return result;

=>

if (!xxx) return www;
...

卫语句要不就从函数中返回,要不就抛出一个异常。

9.6 以多态取代条件表达式

工厂模式

9.7 引入空对象

https://blog.csdn.net/qiumengchen12/article/details/44923139

9.8 引入断言

某一段代码需要对程序状态做出某种假设 => 以断言明确表现这种假设

如果断言失败,代表程序员犯了错误

10、简化函数调用

10.1 函数改名

函数的名称没有揭示函数的用途 => 修改函数名称

函数的名称应该准确的表达它的用途,函数的内容一定不要离函数名太远。

10.2 添加参数

某个函数需要从调用端得到更多的信息 => 为此函数添加一个对象参数,让该对象带进函数所需信息

注:如果有其它可能(例如,通过已有参数可以计算出来),不要添加参数,实在不行再使用本项重构。

10.3 移除参数

函数本体不再需要某个参数 => 将其去除

10.4 将查询函数和修改函数分离

10.5 令函数携带参数

若干函数做了类似的工作,但在函数本体中却包含了不同的值 => 建立单一函数,以参数表达那些不同的值

多个函数干的活类似,只是部分参数不同,将那部分参数作为入参传入。

10.6 以明确函数取代参数

一个函数,完全取决于参数值而采取不同行为 => 针对参数的每一个可能值,建立一个独立函数

void setValue(String name, int value) {
    if (name.equals("height")) {
        _height = value;
        return;
    } 
    if (name.equals("weight")) {
        _weight = value;
        return;
    } 
}

=>

void setHeight(int height) {
    _height = height;
}

void setWeight(int weight) {
    _weight = weight;
}

10.7 保持对象完成

从某个对象中取出若干值,将他们作为某一次函数调用时的参数 => 改为传递整个字段

参数列表要尽可能短,最好可以通过很少的参数获取到所需的信息。

注:如果会是你的依赖结构恶化,那么就不要使用这个重构方法。

10.8 以函数取代参数

对象调用某个函数,并将所得结果作为参数传给另一个函数。而且接受该函数的函数本身也能调用前一个函数 => 让参数接受者去除该项参数,并直接调用前一个函数

如果函数可以通过其它途径获得参数值,那么他就不该通过参数获取该值,过长的参数值会增加阅读者的理解难度。

10.9 引入参数对象

某些参数总是很自然的同时出现 => 以一个对象取代这些参数

10.12 以工厂函数取代构造函数

创建对象时不仅仅是做简单的建构动作 => 将构造函数改为静态工厂方法

10.13 封装向下转型

某个函数返回的对象,需要由函数调用者执行向下转型 => 将向下转型动作转移到函数中。

方法的返回值最好返回客户端需要的类型。

11、处理概况关系

11.8 提炼接口

若干客户使用类/接口中的同一子集 => 将相同的子集提炼到一个独立的接口中

例如 Tomcat的HttpServletRequestWrapper类,实现了Lifecyle接口和HttpServletRequest接口,假设这2个接口是一个接口,但是客户使用时其实只需要用到HttpServletRequest接口的部分,所以将其独立成为一个接口。

如果某个类在不同环境下扮演截然不同的角色,使用接口就是一个好主意。

todo: 我现在还是不会使用接口,需要读源码了啊,但现在能力不足,唉。

11.9 以委托取代继承

某个子类只使用超类中的一部分,或者根本不需要继承而来的数据 => 在子类中新建一个字段用以保存超类;调整子类函数,令他改为委托超类;然后去掉2者之间的关系

赞(0) 打赏
未经允许不得转载:IDEA激活码 » 重构读书笔记

一个分享Java & Python知识的社区