重构-改善既有代码的设计-书摘
重构是在不改变软件可观察行为的前提下改善其内部结构。
设计模式为重构行为提供了目标,模式是你希望到达的目标,重构则是到达之路。重构,第一个案例
- 如果你发现自己需要为程序添加一个特性,而代码结构使你无法很方便地达成目的,那就先重构那个程序,使特性的添加比较容易进行,然后再添加特性。
- 重构之前,首先检查自己是否有一套可靠的测试机制。这些测试必须有自我检验能力。
- 重构技术就是以微小的步伐修改程序,如果你犯下错误,很容易便可以发现它。
- 任何一个傻瓜都能写出计算机可以理解的代码,唯有写出人类容易理解的代码,才是优秀的程序员。
重构原则
- 三次法则:事不过三,三则重构。
- 不要过早的发布接口,请修改你的代码所有权政策,使重构更顺畅。
代码的坏味道
- 重复代码(Duplicated Code)
- 多于一处的相同代码
- 过长函数(Long Method)
- 每当感觉需要以注释来说明点什么的时候,我们就把需要说明的东西写进一个独立函数中,并以其用途命名。
- 条件表达式和循环常常是提炼的信号。
- 过大的类(Large Class)
- 大类内往往出现太多实例变量,可以将彼此相关的变量提炼至新类。
- 过长参数列(Long Parameter List)
- 发散式变化(Divergent Change)
- 一个类受多种变化影响
- 散弹式修改(Shotgun Surgery)
- 一种变化引发多个类相应修改
- 依恋情结(Feature Envy)
- 哪个类拥有最多被此函数使用的数据,然后就把这个函数和那些数据放一起。
- 数据泥团(Data Clumps)
- 删掉众多数据中的一项,如果其他数据不再有意义,你应该把这些数据封装成一个新的对象。
- 基本类型偏执(Primitive Obsession)
- switch惊悚现身(Switch Statements)
- 少用switch case,可以用多态替代。
- 平行继承体系(Parallel Inheritance Hierarchies)
- 冗赘类(Lazy Class)
- 夸夸其谈未来性(Speculative Generality)
- 令人迷惑的暂时字段(Temporary Filed)
- 将临时变量放在实例变量
- 过渡耦合的消息链(Message Chains)
- 中间人(Middle Man)
- 过渡亲昵(Inappropriate Intimacy)
- 异曲同工的类(Alternative Classes with Different Interfaces)
- 不完美的库类(Incomplete Library Class)
- 纯稚的数据类(Data Class)
- 被拒绝的遗赠(Refused Bequest)
- 过多的注释(Comments)
- 当你感觉需要注视时,请先尝试重构,试着让所有注释变得多余。
构筑测试体系
- 确保所有测试都完全自动化,让它们检查自己的测试结果。
- 一套测试就是一个强大的bug侦查器,能够大大缩减查找bug所需要的时间。
- 每当你受到bug报告,请先写一个单元测试来暴露bug。
- 考虑可能出错的边界条件,把测试火力集中在那儿。
重新组织函数
Extract Method
最大的困难就是处理局部变量,而临时变量则是其中一个主要的困难源头。
内联临时变量
- 示例:1234double basePrice = anOrder.hasePrice();return (basePrice > 100)//inlinereturn (anOrder.basePrice() > 1000)
- 示例:
运用
Replace Temp with Query
去掉所有可去掉的临时变量。- 由于临时变量只在所属的函数内可见,所以它们会驱使你写出更长的函数,因为只有这样你才能访问到需要的临时变量。如果把临时变量替代为一个查询,那么同一个类中的所有函数都可以获得这份信息。
- 示例:12345double basePrice = quantity * itemPrice;//extractdouble basePrice(){return quantity * itemPrice;}
如果表达式非常复杂而难以阅读,引入解释性变量
Introduce Explaining Variable
- 我更倾向于提炼成函数,因为可以被所有函数使用。
- 示例:123456if(platform.toUpperCase().indexOf("MAC") > -1){...//extractfinal boolean isMacOs = platform.toUpperCase().indexOf("MAC") > -1;if(isMaOs){...
如果临时变量被赋值超过一次(处理循环变量和结果收集变量),就应该分解临时变量
Split Temporary Variable
。- 示例:123456789double temp = 1.0;System.out.println(temp);temp = 2.0;System.out.println(temp);//replacedouble a = 1.0;System.out.println(a);double b = 2.0;System.out.println(b);
- 示例:
如果你在函数内对参数赋值,就得使用
Remove Assignments to Parameters
。- 因为对参数赋值降低了代码的清晰度,而且混用了按值传递和按引用传递。
- 示例:123456int discount (int inputVal, int quantity, int yearToDate) {if(inputVal > 50) inputVal -=2;//replaceint discount (int inputVal, int quantity, int yearToDate) {int result = inputVal;if(inputVal > 50) result -=2;
如果临时变量太混乱,难以替换。使用
Replace Method with Method Object
。代价是引入一个新类。- 替换算法
Substitute Algorithm
。使用简洁高效的算法。
提炼函数的优点:
- 函数细粒度越小,复用的机会就越大。
- 使高层函数读起来就像一系列注释。
- 函数细粒度越小,覆写也会越容易些。
在对象之间搬移特性
- 如果一个类有太多行为,或如果一个类与另一个类有太多合作而形成高度耦合,就需要搬移函数
Move Method
- 做法:在该函数最常引用的类中建立一个有着类似行为的新函数。将旧函数变成一个单纯的委托函数,或是将旧函数完全的移除。
- 如果发现,对于一个字段,在其所在的类之外的另一个类中有更多函数使用了它,就应该考虑搬移这个字段
Move Filed
。- 做法:在目标类新建一个字段,修改源字段的所有用户,令它们改用新字段。
- 某个类做了应该由两个类做的事,就需要提炼类
Extract Class
- 做法:建立一个新类,将相关的字段和函数从旧类搬移到新类。
- 某个类没有做太多事情,就将这个类的所有特性搬移到另一个类中,然后移除原类。
如果某个客户先通过服务对象的字段得到另一个对象,然后调用后者的函数,那么客户必须知晓这一层委托关系。万一委托关系发生变化,客户也得相应变化。你需要隐藏委托关系
Hide Delegate
- 做法:在服务类上建立客户所需的所有函数,用以隐藏委托关系。
- 示例:12345678910111213141516171819//人class Person{Department department;//getter and setter}//部门class Department {private String chargeCode;private Person manager;//getter and setter}//如果客户需要知道某人的经理是谁,必须先取得Department对象;manager = john.getDepartment().getManager();//在Person类中隐藏委托关系public Person getManager() {return department.getManager();}//修改后manager = john.getManager();
如果你使用的类无法提供你需要的服务,就需要引入外加函数
Introduce Foreign Method
。- 做法:在客户类中建立一个函数,并以第一参数形式传入ige服务类事例。
示例:
123456Date newStart = new Date (previousEnd.getYear(), previousEnd.getMonth(), priviousEnd.getDate()+1);// extractDate new Start = nextDay(previousEnd);private static Date nextDay(Date arg) {return new Date (arg.getYear(), arg.getMonth(), arg.getDate()+1);}如果上诉情况过多,就需要引入本地扩展
Introduce Local Extension
,使用子类扩展或包装类。
重新组织数据
- 自封装字段(Self Encapsulate Filed)
- 为字段建立setter/getter,并且只以这些函数来访问字段。
- 好处:子类可以覆写函数改变获取数据的途径,支持延迟初始化。
- 缺点:难以阅读。
- 以对象取代数据值(Replace Data Value with Object)
- 一个数据项与其它数据和行为一起才有意义,将数据项变成对象。
- 将值对象改为引用对象(Change Value to Reference)
- 从一个类衍生出许多批次相等的实例,希望它们替换为同一个对象,将这个值对象变成引用对象。
- 将引用对象改为值对象(Change Reference to Value)
- 一个引用对象很小且不可变,而且不易管理。将它变成一个值对象。
- 以对象取代数组(Replace Array with Object)
- 一个数组,其中的元素各代表不同的东西,以对象替代数组。
- 字面常量取代魔法数(Replace Magic Number with Symbolic Constant)
- 你有一个字面数值,带有特别含义,创建一个常量,根据其意义命名,并用字面数值替换这个常量。
- 魔法数:具有特使意义,却又不能明确表现出这种意义的数字。
- 一旦这些数发生改变,你就必须找到这些魔法数,并将它们全部修改一遍。
- 示例:12345678double potentialEnerty(double mass, double height){return mass * 9.81 * height;}//replacedouble potentialEnergy(double mass, double height){return mass * GRAVITATIONAL_CONSTANT * height;}static final double GRAVITATIONAL_CONSTANT = 9.81;
简化条件表达式
分解条件表达式(Decompose Conditional)
- 一个复杂的条件语句,从if、then、else三个段落中分别提炼出独立的函数。
- 可以突出条件逻辑,更清楚地表明每个分支的作用,并且突出每个分支的原因。
- 示例:1234567891011if(date.before(UMMER_START) || date.after(SUMMER_END)){charge = quantity * winterRate + winterServiceCharge;} else {charge = quantity * summerate;}//replaceif(notSummer(date)){charge = winterCharge(quantity);} else {charge = summerCharge(quantity);}
合并条件表达式(Consolidate Conditional Expression)
- 一系列的条件测试,都得到相同的结果。将这些测试合并为一个条件表达式,并将这个条件表达式提炼成为一个独立函数。
- 使检查的用意更清晰。
- 示例:123456789double disablilitAmount(){if(seniority < 2) reutrn 0;if(monthsDisabled > 12) return 0;if(isPartTime) return 0;}//replacedouble disabilityAmout(){if(isNotEligableForDisability()) return 0;}
移除控制标记(Remove Control Flag)
- 在一些列布尔表达式中,某个变量带有“控制标记”的作用,以break语句或者return语句取代控制标记。
以卫语句取代嵌套条件表达式(Replace Nested Conditional with Guard Clauses)
- 如果两条分支都是正常行为,就应该使用if…else…的条件表示式,如果某个条件极其罕见,就应该单独检查该条件,并在条件为真时立刻从函数中返回。这种单独检查常常被称为“卫语句”。
- 示例:123456789101112131415161718192021222324double getPayAmout(){double result;if(isDead){result = deadAmount();} else {if(isSeparated){result = separatedAmount();} else {if(isRetired){result = retiredAmount();} else {result = normalPayAmount();}}}return result;}//replacedouble getPayAmount(){if(isDead) return deadAmount();if(isSeparated) return separatedAmount();if(isRetired) return retiredAmount();return normalPayAmount();}
以多态取代条件表达式(Replace Conditional with Polymorphism)
- 如果尼需要根据对象的不同类型而采取不同的行为,多态使你不必编写明显的条件表达式。
- 引入Null对象(Introduce Null Object)
- 你需要再三检查某个对象是否为null。将null值替换为null对象。
- Java 8 已经引入了Optional类。
- 引入断言(Introduce Assertion)
- 某一段代码需要对程序状态做出某种假设,以断言明确表现这种假设。
- 断言是一个条件表达式,应该总是为真。如果它失败,表示程序员犯了错误。因此断言的失败应该导致一个非受控异常(unchecked exception)。断言绝对不能被系统的其它部分使用。程序最后的成品往往将断言删除。
- 在交流的角度上,断言可以帮助程序阅读这理解代码所做的假设;在调试的角度上,断言可以在距离bug最近的地方抓住它们。
- 请只使用它来检查“一定必须为真”的条件。
- 示例:123456789double getExpenseLimit(){//should have either expense limit or a pirmary projectreturn (expenseLimit != NULL_EXPENSE)? expenseLimit:primaryProjct.getMemberExpenseLimit();}//replacedouble getExpenseLimit(){Assert.isTrue(expenseLimit != NULL_EXPENSE || primaryProject != null);return (expenseLimit != NULL_EXPENSE)? expenseLimit:primaryProjct.getMemberExpenseLimit();}
简化函数调用
函数改名(Rename Method)
- 技巧:首先应该考虑给这个函数写上一句怎样的注释,然后想办法将注释变成函数名称。
- 如果旧函数是public的接口,你可能无法安全地删除它,这种情况最好保留在原地,并将它标记为deprecated(不建议使用)。
- 示例:12345678910public String getTelephoneNumber(){reutrn ("(" + officeAreaCode + ") " + officeNumber);}//replacepublic String getTelephoneNumber(){return getOfficeTelephoneNumber();}public String getOfficeTelephoneNumber(){reutrn ("(" + officeAreaCode + ") " + officeNumber);}
添加参数(Add Parameter)
- 移除参数(RemoveParameter)
- 将查询函数和修改函数分离(Separate Query from Modifier)
- 某个函数既返回对象的状态值,又修改对象的状态。建立两个不同的函数,其中一个负责查询,另一个负责修改。
- 如果一个函数只提供一个值,那么是很好的,应该你可以任意调用这个函数,没有副作用。
- 多线程环境下,需要synchoronized分离出的方法,保证一致性。
保持对象完整(Preserve Whole Object)
- 你从某个对象中取出若干值,将它们作为某一次函数调用的参数。改为传递整个对象。
- 优点:如果将来需要修改参数,就可以避免修改。同时减少了参数长度。
- 缺点:函数与对象之间耦合,应该考虑将函数一道对象中。
- 示例:12345int low = daysTempRange().getLow();int hight = daysTempRange().getHigh();withinPlan = plan.withinRange(low. high);//replacewithinPlan = plan.withinRange(daysTempRange());
隐藏函数(Hide Method)
- 一个类从来没有被其他任何类用到,将这个函数修改为private。
- 尽可能降低函数的可见度。
工厂函数取代构造函数(Replace Constructor with Facotory Method)
- 你希望在闯将对象时不仅仅做简单的构建动作。将构造函数替换为工厂函数。
- 示例:1234567Employee(int type){this.type = type;}//replacestatic Employee create(int type){return new Employee(type)}
以异常取代错误码(Replace Error Code with Exception)
- 将“普通程序“”与“错误处理”分开,使程序更容易理解。
- 如果调用者有责任在调用钱检查必须状态,就抛出非受控异常(unchecked)
- 如果想抛出受控异常(checked),你可以新建一个异常类,也可以使用现有的异常类。
- 示例:12345678910111213int vithdraw(int amount){if(amount > balance)return -1;else {balance = amount;return 0;}}//replacevoid withdraw(int amount) throws BalanceException{if(amount > balance) throw new BalanceException();balance = amount;}
处理概括关系
- 字段上移:两个子类拥有相同的字段,将该字段移至超类。
- 函数上移:有些函数,在各个子类中产生完全相同的结果,将函数移至超类。
- 提炼超类:两个类有相似特性,为这两个类建立一个超类,将相同特性移至超类。