《重构 改善既有代码的设计 2》重新组织函数、数据

/**
 @startTime 2020-12-20 15:30
 @endTime 2020-12-20 21:00
 @startPage 103 
 @endPage 130
 @efficiency 130/5 = 26页/天
 @needDays 412/26 = 16天
 @overDay 2020-12-16 + 16天 = 2020-12-31
*/

第五章 重构列表

第5~12章构成了一份重构列表草案,其中所列的重构手法来自作者数年的编程心得。

1、重构的记录格式

  1. 首先是名称(name)。建造一个重构词汇表,名称是很重要的。
  2. 名称之后是一个简短概要(summary)。简单介绍此重构手法的适用场景,以及它所做的事情。这部分可以帮助你更快找到你所需要的重构手法。
  3. 动机(motivation)为你介绍“为什么需要这个重构”和“什么情况下不该使用这个重构”。
  4. 做法(mechanics)简明扼要地一步一步介绍如何进行此重构。
  5. 范例(examples)以一个十分简单的例子说明此重构手法如何运作。

2、寻找引用点

不要盲目的进行查找-替换,应该检查每一个引用点,确保它的确指向你想要替换的东西。
可以让编译器帮助你捕捉引用点,但是这也会存在一些问题,比如:

  1. 如果被删除的部分在继承体系中声明不止一次,那么编译器也会被迷惑。
  2. 编译器可能太慢。
  3. 编译器无法找到通过反射机制得到的引用点

3、这些重构手法有多成熟

小步前进、频繁测试。
设计模式,为重构提供了目标。
模式和重构之间有着一种与生俱来的关系,模式是你希望达到的目标,重构则是到达之路。

第六章 重新组织函数

我的重构手法中,很大一部分是对函数进行整理,使之更恰当地包装代码。

1、提炼函数

将一段代码放进一个独立函数中,并让函数名称解释该函数的用途。
「动机」

  1. 如果每个函数的粒度都很小,那么函数被复用的机会就会很大;
  2. 使高层函数读起来就像一系列注释;
  3. 如果函数都是细粒度的,那么函数的覆写也会更容易些;
    一个函数多长算合适?在我看来,长度不是问题,关键在于函数名称和函数本体之间的语义长句。如果提炼可以强化代码的清晰度,那么就去做,就算函数名称比提炼出来的代码还长也没无所谓。

2、内联函数

在函数调用点插入函数本体,然后移除该函数。

3、内联临时变量

将所有对该变量的引用动作,替换为对它赋值那个表达式本身。

4、以查询取代临时变量

将这个表达式提炼到一个独立函数中,将这个临时变量的所有引用点替换为对新函数的调用。此后,新函数就可被其他函数使用。

5、引入解释性变量

将复杂表达式的结果放进一个临时变量,以此变量名称来解释表达式用途。
「动机」
在条件逻辑中,引入解释性变量特别有价值,你可以用这项重构将每个条件子句提炼出来,以一个良好命名的临时变量来解释对应条件子句的意义。使用这项重构的另一种情况是,在较长算法中,可以运用临时变量来解释每一步运算的意义。

6、分解临时变量

针对每次赋值,创造一个独立、对应的临时变量。
「动机」
如果临时变量被赋值超过一次,就意味着它们在函数中承担了一个以上的责任。如果临时变量承担多个责任,它就应该被替换为多个临时变量,每个变量只承担一个责任。同一个临时变量承担两件不同的事情,会另代码阅读者糊涂。

/**
 @startTime 2020-12-21 21:30
 @endTime 2020-12-21 23:50
 @startPage 131 
 @endPage 188
 @efficiency 188/6 = 31.3页/天
 @needDays 412/31.3 = 13天
 @overDay 2020-12-16 + 13天 =  2020-12-28
*/

7、移除对参数的赋值

以一个临时变量取代该参数的位置。
首先,我要确定大家都清楚“对参数赋值”这个说法的意思。如果你把一个名为user的对象作为参数传给某个函数,那么“对参数赋值”意味着改变user,使它引用另外一个对象。如果你在“被传入对象”身上进行什么操作,那没问题,我也总是这样干。我只针对“user被改而指向另一个对象”这种情况来讨论。

8、以函数对象取代函数

你有一个大型函数,其中对局部变量的使用使你无法采用提取函数,将这个函数放进一个单独对象中,如此一来局部变量就成了对象内的字段,然后你可以在同一个对象中将这个大型函数分解为多个小型函数。
「动机」
我在本书中不断向读者强调小型函数的优美动人。只要将相对独立的代码从大型函数中提炼出来,就可以大大提高代码的可读性。
「做法」

  1. 建立一个新类,根据待处理函数的用途,为这个类命名。
  2. 在新类中建立一个final字段,用以保存原先大型函数所在的对象。我们将这个字段称为“源对象”。同时,针对原函数的每个临时变量和每个参数,在新类中建立一个对应的字段保存之。
  3. 在新类中建立一个构造函数,接收源对象及原函数的所有参数作为参数。
  4. 在新类中建立一个compute函数。
  5. 将原函数的代码复制到compute()函数中。如果需要调用源对象的任何函数,请通过源对象字段调用。
  6. 编译。
  7. 将旧函数的函数本体替换为compute()函数。

9、替换算法

将函数本体替换为另一种算法。

第七章 在对象之间搬移特性

1、搬移函数

你的程序中,有个函数与其所在类之外的另一个类进行更多的交流,在该函数最常引用的类中建立一个有着类似行为的新函数。将旧函数变成一个单纯的委托关系,或是将旧函数完全移除。
“搬移函数”是重构理论的支柱。如果一个类有太多行为,或如果一个类与另一个类有太多合作而形成高度耦合,我就会搬移函数。通过这种手段,可以使系统中的类更简单,这些类最终也将更干净利落地实现系统交付的任务。

2、搬移字段

3、提炼类

某个类做了应该由两个类做的事,建立一个新类,将相关的字段和函数从旧类搬移至新类。

4、将类内联化

某个类没有做太多的事情,将这个类的所有特性搬移至另一个类中,然后移除该类。

5、隐藏委托关系

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

public Person getManager(){
	return _department.getManager();
}

只有完成了对Department所有函数的委托关系,并相应修改了Person的所有客户,我就可以移除Person中的访问函数getDepartment()了。

6、移除中间人

某个类做了过多的简单委托动作时,让客户直接调用受委托类。

7、引入外加函数

Date start = new Date(previous.getYear(),previous.getMonth(),previous.getDate()+1);

变为

Date start = nextDay(previous);
private static Date nextDay(previous.getYear(),previous.getMonth(),previous.getDate()+1);

貌似懂了一点。
外加函数终归是权宜之计,如果有可能,仍然应该讲这些函数搬移至它们应该在的地方。

8、引入本地扩展

你需要为服务类提供一些额外函数,但你无法修改这个类,建立一个新类,使它包含这些额外函数,让这个扩展类成为原类的子类或包装类。
「动机」
在子类和包装类之间做选择时,我通常首选子类,因为这样的工作量比较少。制作子类的最大障碍在于,如果原数据是可修改的,一个修改动作无法改变两个副本,这时就必须改用包装类
「做法」

  1. 建立一个扩展类,将它作为原始类的子类或包装类。
  2. 在扩展类中加入转型构造函数,所谓转型构造函数,是指“接受原对象作为参数”的构造函数。如果采用子类化方案,那么转型构造函数应该调用适当的超类构造函数;如果采用包装类方案,那么转型构造函数应该将它得到的传入参数以实例变量的形式保存起来,用作接受委托的原对象。
  3. 在扩展类中加入新特性。
  4. 根据需要,将原对象改为扩展对象。
  5. 将针对原始类定义的所有外加函数搬移至扩展类中。

第八章 重新组织数据

1、自封装字段

为字段建立取值/设置函数,并且只以这些函数来访问字段。

2、以对象取代数据值

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

3、将值对象改为引用对象

4、将引用对象改为值对象

你有一个引用对象,很小且不可变,而且不易管理,此时,将它变成一个值对象。
要在引用对象和值对象之间做出选择,有时并不容易。做出选择后,你常会需要一条回头路。
如果引用对象开始变得难以使用,也许就应该将它变为值对象。引用对象必须被某种方式控制,你总是必须向其控制者请求适当的引用对象。它们可能造成内存区域之间错综复杂的关联。在分布式系统和并发系统中,不可变的值对象特别有用,因为你无需考虑它们的同步问题。

5、以对象取代数组

以对象取代数组,对于数组中的每一个元素,以一个字段来表示。
「动机」
数组是一种常见的用以组织数据的结构。不过,它们应该只用于“以某种顺序容纳一组相似对象”。有时你会发现,一个数组容纳了多种不同对象,这会给用户带来麻烦,因为它们很难记住像“数组的第一个元素是人名”这样的约定。对象就不同了,你可以运用字段名称和函数名称来传达这样的信息,也无需使用注释。而且使用对象,你还可以将信息封装起来,并使用“搬移函数”将它加上相关行为。
我之前做过一个数据上传系统,有这么个结构:

Map<String,String[]> hashMap = new HashMap<String,String[]>();
String[] data = new Data[4];
data[0] = "1";//状态state
data[1] = "1";//延展状态exstate
data[2] = "1";//数值
data[3] = "2020-12-21 23:22:00";//数据生成时间
String key = "01A01";//测点编号
hashMap.put(key,data);
...
for(String key : hashMap.keySet){
	//状态state、延展状态exstate、数值、数据生成时间
	String[] data = hashMap.get(key);
	String state = data[0];
	String exstate = data[1];
	String value = data[2];
	String time = data[3]'
}

因为数据存在基本数据、实时数据、分钟累计数据,这种结构的代码项目中比比皆是,写的那叫一个烂,这个上传程序折磨的我苦不堪言,往事不堪回首。
「做法」

@Data
public class BasicData(){
	private int state;
	private int exstate;
	private double value;
	private Date time;
}

Map<String,BasicData> hashMap = new HashMap<String,BasicData>();
BasicData basicData = new BasicData();
basicData.setState = "1";
basicData.setExstate = "1";
basicData.setValue = 3.12;
String str = "2020-12-21 23:22:00";
basicData.setTime = DateUtil.StringToDate(str,"yyyy-MM-dd hh:mm:ss");
String key = "01A01";//测点编号
hashMap.put(key,basicData);
...
for(String key : hashMap.keySet){
	//状态state、延展状态exstate、数值、数据生成时间
	BasicData basicData = hashMap.get(key);
	String state = basicData.getState();
	String exstate = basicData.getExstate();
	double value = basicData.getValue();
	Date time = basicData.getTime();
}

多么痛的领悟。
其实很简单。

/**
 @startTime 2020-12-22 21:30
 @endTime 2020-12-22 22:50
 @startPage 189 
 @endPage 235
 @efficiency 235/7 = 33.6页/天
 @needDays 412/33.6 = 12天
 @overDay 2020-12-16 + 12天 =  2020-12-27
*/

6、复制“被监视数据”

你有一些领域数据置身于GUI控件中,而领域函数需要访问这些数据。将该数据复制到一个领域对象中。建立一个Observer模式,用以同步领域对象和GUI对象内的重复数据。
「动机」
一个分层良好的系统,应该讲处理用户界面和处理业务逻辑的代码分开。之所以这样做,原因有以下几点:

  1. 你可能需要使用不同的用户界面来表现相同的业务逻辑,如果同时承担两种责任,用户界面会变得过分复杂;
  2. 与GUI隔离之后,领域对象的维护和演化都会变得更容易,你甚至可以让不同的开发者负责不同部分的开发。

7、将单向关联改为双向关联

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

8、将双向关联改为单向关联

去掉不必要的关联。

9、以字面常量取代魔法数

你有一个字面数值,带有特殊的含义。创造一个常量,根据其意义为它命名,并将上述的字面数值替换为这个常量。

10、封装字段

将字段封装为private,并提供相应的访问函数。

11、封装集合

12、以数据类取代记录

记录型结构是许多编程环境的共同性质,你可能处理一些遗留程序,也可能需要通过传统API与记录结构交流,或者是从数据读出的记录。这些时候你就有必要创建一个接口类,用以处理这些外来数据。将这些数据搬移到Java的bean中。

13、以类取代类型码

14、以子类取代类型码

借助多态处理变化行为。

15、以state/strategy取代类型码

你有一个类型码,它会影响类的行为,但你无法通过继承手法消除它。此时,就可以以状态对象取代类型码。

16、以字段取代子类

你的各个子类的唯一差别就只在“返回常量数据”的函数身上,修改这些函数,使它们返回超类中的某个新增字段,然后销毁子类。

上一篇:《重构 改善既有代码的设计 1》重构原则
下一篇:【编写高质量代码:改善Java程序的151个建议】

  • 3
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 6
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

哪 吒

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值