近期在看《重构:改善既有代码的设计》这本书,目前读了几个章节的内容,里面的内容与案例还是比较贴合实际且阅读难度不算大的一本重构书籍。好记性不如烂笔头,为了与大家分享和加深印象特记录本篇文章,文章会跟随我阅读的进度进行持续更新和补充,如有不足之处请在下方评论指点,本人会及时更正以免影响其他同学阅读。
注:项目内引入CheckStyle会让你养成写出优雅代码的习惯。
《重构:改善既有代码的设计》这本书在各大论坛上被指出是Java程序员必读的三本书之一。(另外两本是《Java编程思想》和《Effective Java》)。其实这本书给我的感觉,不一定局限于Java开发的同学,里面的思想和理念适合多种编程语言的设计。本书的讲解方式是贯穿式的,每个章节都会运用到其他章节的某种重构手法并标记出具体页码,刚开始看的时候还有些不适,总是要翻来翻去,像极了在开发时多个方法互相跳转的模式。
本书除了引导你感悟什么代码需要重构,使用何种重构方式之外,还会搭配代码片段更生动讲解各个重构方式的思想与优缺点。
本书第一章以一个影片出租店的程序逻辑为主导展开的第一个重构,这个案例的重构没有什么难点,仅仅是让大家对重构有一个初步的认识和理解。(由于过于本章代码过于简单,就不展现在这了)
本章节主要讲解的总结如下:
public int getFrequentRenterPoints() {
int frequent = 1;
if (getMovie().getPriceCode() == Movie.NEW_RELEASE && daysRented > 1) {
frequent++;
}
return frequent;
}
可改成:
public int getFrequentRenterPoints() {
if (getMovie().getPriceCode() == Movie.NEW_RELEASE && daysRented > 1) {
return 2;
}
return 1;
}
1.重构的定义
通俗的讲就是现有的项目进行内部结构的调整,无论是接手过来的项目,还是因为之前能力有限无法写出优雅的代码,其宗旨都是在不改变现有业务流程的基础上进行改进与优化。
本章提到了软件开发的两顶帽子,阐述了大家日常开发的两种模式。第一,在添加一个崭新的功能时,不应该去触碰现有的代码,而是只管添加新功能并进行测试。第二,重构阶段不再添加新的功能,只管对原有程序进行改进,最终通过现有的测试用例。
2.为何重构
3.何时重构
4.何时不该重构
这个章节还是比较重要的,但由于这本书的发版时间比较早,有些重构手法已经成了开发中的一些常识,本章会介绍如何识别出代码中存在的坏味道,为之后正确的重构打好基础。经典的坏味道特性总结在下面,当然不局限与此。
1. 重复代码
2. 过长函数
每当感觉一段代码逻辑复杂并过长需要一个注释来说明的时候,优先选择把这段要说明的东西提炼成一个单独的函数专门去做这件事,注意命名要尽量贴合函数用途。
3. 过大的类
类的设计应当遵循设计模式中的单一职责(SRP)。重构这种类可以尝试用抽取接口的方式思考如何拆解。
4. 过长的参数列
5. 发散式变化
发散式变化体现在一个类会受到各种变化的影响,牵一发而动全身。这个对我体会较深,在工作中每个项目组要求代码质量的水准都不同,开发人员往往容易把全部逻辑放在Service层,甚至是Controller层。这样不断的扩展最后将造成很难维护,扩展风险及大。
6. 散弹式修改
散弹式修改指的是一个小小的变化导致多个类都需要对应去做出调整,应该把需要修改的部分放到一个类中统一做出处理,避免遗漏。
7. 依恋情节
将数据和对数据操作的行为包装在一起。通俗的讲就是将一起变化的东西放在一块。
8. 数据泥团
两个类中相同的字段、函数中相同的参数,考虑提取成一个单独的数据类。
9. 夸夸其谈未来性
检查抽象类、委托、方法的参数没有实际作用的,那么就果断删除掉不要未雨绸缪。
本章节比较简短,主要围绕Junit测试模式为主,对重构的地方进行测试。有时,为了提高效率不如试试Groovy测试框架。本人用过一段时间的Groovy来对功能进行测试,给我的感觉Groovy比起Java更简洁、开发效率更高,由于元编程的特性可能会让Groovy性能有所损失(10%左右)。
本人整理的Groovy简单使用,当然Groovy的强大不局限于此
本章没有重点,如果是Java开发,使用Idea就能够很好的支持常见的重构说法,还有各种自动提示,底色标黄处都值得你去留意一下,是否潜在异常。
本章提到9种针对函数的优化做法,我将列举几种常用的配合代码案例进行整理。来吧,展示:
1. 内联函数: 在函数中调用处插入函数代码体,将原函数删除。
int value = dto.getLargerThanFile();
int getRating(){
return (largerThanFile()) ? 2 : 1;
}
Boolean largerThanFile(){
return value > 6;
}
//重构后
int getRating(){
return (value > 6) ? 2 : 1;
}
2. 引入解释性变量: 将复杂表达式提取存放一个临时变量中,并命名来表达此用途。
if((name.indexof("mack")>-1)&&(readBook.indexof("重构")>-1)) {
......;
}
//重构后
Boolean isName = name.indexof("mack") > -1;
Boolean isBook = readBook.indexof("重构") > -1;
if(isName &&isBook) {
......;
}
3. 以查询取代临时变量: 以一个临时变量存储运算得出的结果,将这部分的运算提炼到独立的函数中。即便算法后续有任何的改动,只需要修改此函数即可。
double basePrice = (principal * interestRate + loan) / 12;
if (basePrice > 1000) {
return basePrice * 0.95;
} else {
return basePrice * 0.98;
}
//重构后
if (getPasePrice() > 100) {
return getPasePrice() * 0.95;
}
else {
return getPasePrice() * 0.98;
}
int getPasePrice() {
return (principal * interestRate + loan) / 12;;
}
面向对象的设计中,“决定把责任放在哪里”是最重要的理念之一。开发中最常见的烦恼是:我们无法从一开始就保证所有的事情做的天衣无缝。在这种情况下,可以尝试一次大胆的重构,改变原来的设计使得代码更加优雅。由于代码实现比较简单,这章就不做代码展示了,这里就介绍一下几种重构手法的思想:
1. 移动函数
类行为尽量做到单一,如果一个类的行为过多,或与其他类有太多的耦合,这时需要做一次搬移。
2. 搬移字段
一个类的字段在另一个类中频繁使用过,考虑将字段搬移。
3. 提炼类
类应该是清楚的抽象,处理一些明确的职责,不应太冗余。
4. 类内联化
它和提炼类刚好相反,如果一个雷不再承担足够的责任、不再有单独存在的理由,可以将这种“萎缩类”塞入另一个类中。
5. 隐藏委托关系
在服务类建立客户所需的所有类,用来隐藏委托的关系。
A–>B
A–>C
进行重构
A–>B–>C
6. 移除中间人
跟隐藏委托关系的手法相反,根据不同的使用情况来判断运用哪种手法。
A–>B–>C
A–>B
A–>C
7. 引入外加函数
当你需要为提供服务的类增加一个函数时,可将这种场景提出来。
Data newStart = new Date(pre.getYear(), pre.getMonth(), pre.getDate() + 1);
//重构后
Date newStart = nextDay(pre);
private static Date nextDay(Date arg) {
return new Date(arg.getYear(), arg.getMonth(), arg.getDate() + 1);
}
1. 自封装字段
问题:直接访问一个字段,但是字段之间的耦合关系逐渐变得笨拙。
优化:为这个字段建立取值、设值函数,并且只以这些函数来访问字段,体现灵活性。
//案例
boolean includes(int arg){
return arg>=low&&arg<=high;
}
//重构
private int low,high;
boolean includes(int arg){
return arg>=getLow()&&arg<=getHigh();
}
int getLow(){return low;}
int getHigh(){return high;}
2. 以对象取代数据值
问题:一个数据项,需要与其他数据、行为一起使用才有意义。
优化:将数据项变成对象形式
3. 以对象取代数组
问题:一个数值,每个元素都代表不同的含义。
优化:以对象代替数组,对数组中每一个元素以一个字段来表示,一劳永逸。
String[] row=new String[2];
row[0] = "张三";
row[1] = "25";
//重构为:
User row = new User();
row.setName("张三");
row.setWins("25");
4. 将单向关联改为双向关联
问题:两个类之间有双向关联,但其中一个类现在不再需要另一个类的特性。
优化:去除不必要的关联,大量的双向链接容易造成“僵尸对象”,双向关联迫使两个对象有依赖关系,对其中一个进行修改会引发另一个类的变化。
5. 以字面常量取代魔法数字
问题:一个字面数值,带有特别的含义。
优化:创建一个常量,根据其行为为它命名,并将上述的字面数值替换为这个常量。
double potentialEnergy(double mass, double height) {
return mass * 9.81 * height;
}
//重构
double potentialEnergy(double mass, double height) {
return mass * GRAVITATIONAL_CONSTANT * height;
}
static final double GRAVITATIONAL_CONSTANT = 9.81;
6. 以字段取代子类
问题:每个子类的唯一差别,只在“返回常量”的函数身上。
优化:修改这些函数,将返回超类中的某个字段,然后销毁子类。
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 _isMale;
...
}
本章提供了一些重构的经典手法,专用来简化一些复杂难以理清的条件逻辑。
1. 分解条件表达式
问题:开发功能点初期经常会产生一些复杂的条件,如(if-then-else)语句。
优化:将if、then、eles段落中分别提炼出相对独立的函数。
if (date.before(SUMMER_START) || date.after(SUMMER_END)) {
charge = quantity * this.winterRate + this.winterServiceCharge;
} else {
charge = quantity * this.summerRate;
}
//重构
//思路:将每个分支拆开,提炼为一个个独立函数
if (notSummer(date)) {
charge = winterCharge(quantity);
} else {
charge = summerCharge(quantity);
}
pirvate boolean notSummer(Date date) {
return date.before(SUMMER_START) || date.after(SUMMER_END);
}
private double summerCharge() {
return quantity * this.summerRate;
}
private double winterCharge() {
return quantity * this.winterRate + this.winterServiceCharge;
}
2. 合并条件表达式
问题:你有一系列条件测试,都得到相同结果。
优化:将这些测试合并为一个条件表达式,并将这个条件表达式提炼成一个独立的函数。
double disabilityAmount() {
if (this.seniority < 2) return 0;
if (this.monthsDisabled > 12) return 0;
if (this.isPartTime) return 0;
...
}
//重构
//思路:将一系列都在做一件事的条件检查,进行联系和拼接
double disabilityAmount() {
if (isNotEligibleForDisability()) return 0;
...
}
boolean isNotEligibleForDisability() {
reutrn ((this.seniority < 2) || (this.monthsDisabled > 12) || (this.isPartTime));
}
3. 合并重复的条件片段
问题:条件表达式冗余,每个分支上都有一些相同的代码。
优化:将重复代码提炼到表达式之外。
if (isSpecialDeal()) {
total = price * 0.95;
send();
} else {
total = price * 0.98;
send();
}
//重构
if (isSpecialDeal()) {
total = price * 0.95;
} else {
total = price * 0.98;
}
send();
4. 移除控制标记
问题:在一系列布尔表达式中,某个变量带有“控制标记”的作用。
优化:以break语句或return语句取代控制标记。
void checkSecurity(String[] people) {
boolean found = false;
for (int i = 0; i < people.length; i++) {
if (!found) {
if (people[i].equals("Don")) {
sendAlert();
found = true;
}
if (people[i].equals("John")) {
sendAlert();
found = true;
}
}
}
}
//重构
void checkSecurity(String[] people) {
for (int i = 0; i < people.length; i++) {
if (people[i].equals("Don")) {
sendAlert();
break;
}
if (people[i].equals("John")) {
sendAlert();
break;
}
}
}
5. 以多台取代条件表达式
问题:项目中会有一些条件表达式,它根据对象类型的不同而选择不同的行为。
优化:将这个条件表达式的每个分支放进一个子类内的覆写函数中,然后将原始函数声明为抽象函数。
如果同一组表达式在程序许多地点出现,那么使用多态的收益是最大。使用条件表达式时,如果你想添加一种新类型,就必须查找并更新所有条件表达式。但如果改用多态,只需建立一个新的子类,并在其中提供适当的函数就行了。类的用户不需要了解这个子类,这就大大降低了系统个部分之间的依赖,是系统升级更加容易。
class Employee {
Employee(EmployeeType type) {
setType(type);
}
int getType() {
return this.type.getTypeCode();
}
void setType(EmployeeType arg) {
this.type = arg;
}
private EmployeeType type;
int payAmount() {
switch(getType()) {
case EmployeeType.ENGINEER:
return this.monthlySalary;
case EmployeeType.SALESMAN:
return this.monthlySalary + this.commission;
case EmployeeType.MANAGER:
return this.monthlySalary + this.bonus;
default:
throw new RuntimeException("Incorrect Employee");
}
}
}
abstract class EmployeeType {
abstract int getTypeCode();
}
class Engineer extends EmployeeType {
int getTypeCode() {
return ENGINEER;
}
}
class Manager extends EmployeeType {
int getTypeCode() {
return SALESMAN;
}
}
class Salesman extends EmployeeType {
int getTypeCode() {
return MANAGER;
}
}
//重构
class EmployeeType {
int payAmount(Employee emp) {
switch (code) {
case ENGINNER:
return emp.getMonthlySalary();
case SALESMAN:
return emp.getMonthlySalary() + emp.getCommission();
case MANAGER:
return emp.getMonthlySalary() + emp.getBenus();
default:
throw new IllegalArgumentException("Incorrect Employee Code");
}
}
}
class EmployeeType {
abstract int payAmount(Employee emp);
}
class Enginner extend EmployeeType {
int payAmount(Employee emp) {
return emp.getMonthlySalary();
}
}
class Salesman extend EmployeeType {
int payAmount(Employee emp) {
return emp.getMonthlySalary() + emp.getCommission();
}
}
class Manager extend EmployeeType {
int payAmount(Employee emp) {
return emp.getMonthlySalary() + emp.getBenus();
}
}
6. 引入断言
问题:一段代码需要对程序状态做出某种假设。
优化:以断言明确表现这种假设。
使用断言明确标明对输入条件的严格要求和限制,单恋可以辅助交流和测试。
double getExpenseLimit () {
return (_expLimit != NULL_EXPENSE) ? _expLimit : _primaryPro.getExpenseLimit();
}
//重构
double getExpenseLimit () {
Assert.isTrue((_expLimit != NULL_EXPENSE) || _primaryPro != NULL );
return (_expLimit != NULL_EXPENSE) ? _expLimit : _primaryPro.getExpenseLimit();
}
在对象技术中,最重要的概念“接口”(interface)。容易被理解和被使用的接口,是开发良好面向对象软件的关键。本章会依次介绍一些使接口变得更简洁易用的重构手法,其实概念很简单但会在开发中经常被忽略。
1. 函数改名
问题:函数的名称未能表达函数的用途。
优化:修改函数名称。
个人补充:在开发初期阶段,一些函数功能不完善使得最初的命名与最终的函数用途不一致,建议在codeReview过程中找到并修改
2. 添加参数
问题:某个函数需要从调用端得到更多信息。
优化:为此函数添加一个对象参数,让该对象带进函数所需信息。
个人补充:当函数入参达到5个时,就要考虑是否将参数进行封装,以对象行形式传入函数。
3. 移除参数
问题:函数本体不再需要某个函数。
优化:将无引用参数进行删除,以免后期维护产生疑惑。
4. 以明确函数取代参数
问题:你有一个函数,其中完全取决于参数值而采取不同行为。
优化:针对该参数的每一个可能值,建立一个独立函数。
static final int ENGINEER = 0;
static final int SALESMAN = 1;
static final int MANAGER = 2;
static Employee create(int type) {
switch (type) {
case ENGINEER:
return new Engineer();
case SALESMANA:
return new Salesman();
case MANAGER:
return new Manager();
default:
throw new IllegalArgumenntException("Incorrect type code value");
}
}
//重构
static Employee createEngineer() {
return new Engineer();
}
static Employee createSalesman() {
return new Salesman();
}
static Employee createManager() {
return new Manager();
}
static Employee create(int type) {
switch (type) {
case ENGINEER:
return Employee.createEngineer();
case SALESMANA:
return Employee.createSalesman();
case MANAGER:
return Employee.createManager();
default:
throw new IllegalArgumenntException("Incorrect type code value");
}
}
5. 封装向下转型
问题:某个函数返回的对象,需要由函数调用者执行向下转型。
优化:将向下转型动作移到函数中。
Object lastReading() {
return readings.lastElement();
}
//重构:当拥有一个集合时,上述那么做就很有意义。
Reading lastReading() {
return (Reading) readings.lastElement();
}
1. 字段上移
问题:两个子类拥有相同的字段。
优化:将该字段移至超类。
2. 函数上移
问题:有些函数,在各个子类中产生完全相同的结果。优化:将该函数移至超类。
优化:将该函数移至超类。
3. 提炼子类
问题:超类中的某个字段只被部分(而非全部)子类用到。优化:将这个字段移到需要它的那些子类去。
优化:将这个字段移到需要它的那些子类去。
4. 提炼接口
问题:若干客户使用类接口中的同一子集,或者两个类的接口有部分相同。优化:将相同的子集提炼到一个独立接口中。
优化:将相同的子集提炼到一个独立接口中。
①该方法仅仅用于按照时间计算费用,以及判断元购会否有特殊技能的用途
double charge(Employee emp, int days) {
int base = emp.getRate() * days;
if (emp.hasSpecialSkill()) {
return base * 1.05;
}
return base;
}
②除了提供员工信息之外,Employee还有很多其他功能,这时可将charge仅涉及的两个功能定义为接口并实现。
interface Billable {
public int getRate();
public boolean hasSpecialSkill();
}
clas Employee implements Billable ...
double charge(Billable emp, int days) {
int base = emp.getRate() * days;
if (emp.hasSpecialSkill()) {
return base * 1.05;
}
return base;
}
1. 将过程化设计转化为对象设计
问题:有一些传统过程化风格的代码。
优化:将数据记录变成对象,将大块的行为分成小块,并将行为移入相关对象之中。
2. 将领域和表述/显示分离
问题:某些GUI类之中包含了领域逻辑。
优化:将领域逻辑分离出来,为它们建立独立的领域类。
为每个窗口建立一个领域类。
略
本书主要涉及重构中的各种细节问题,基本都是比较常见的开发中遇到的常常被人忽略的一个个小点。本书代码案例都是比较简单的片段来说明对应说法的思想,整体看下来还是比较容易理解的。
①从如何识别代码的坏味道
②重新组织函数、对象、数据
③简化表达式、简化函数调用
④处理概况(继承)关系
⑤大型重构
⑥结合实际
面对项目中已有代码,有些模块还是有点不知所措,我觉得还是欠缺一些思考,以及对项目的整体把控不够。接下来的时间在逐渐熟练掌握项目的同时,也将这些重构手法运用到项目当中。