《重构 改善既有代码的设计》——学习笔记(一)
来源:互联网 发布:远控软件下载 编辑:程序博客网 时间:2024/06/07 05:52
代码的坏味道
Duplicated Code(重复代码)
- 同一个类的两个函数含有相同的表达式,需要将重复的这段代码提出来,让这两个函数都调用这段代码
- 两个互为兄弟的子类内含相同表达式,需要将代码提炼出来放入父类中
- 如果两个毫不相关的类出现Duplicated Code,需要将重复代码提炼到一个独立类中,然后在另一个类内使用这个新类,抑或这个函数可能属于第三个类,而另两个类应该引用这第三个类
Long Method(过长函数)
拥有短函数的对象会活得比较好、比较长。解释能力、共享能力、选择能力——都是由小型函数支持的。每当感觉需要以注释来说明点什么的时候,就把需要说明的东西写进一个独立函数中,并以其用途命名
Large Class(过大的类)
一个类如果拥有太多代码,就需要将其拆分,可以先确定客户端如何使用它们,然后为每一种使用方式提炼出一个接口
Long Parameter List(过长参数列)
太长的参数列难以理解,太多参数会造成前后不一致、不易使用,而且一旦需要更多数据,就不得不修改它,可以把函数所需要的东西通过对象传入
Divergent Change(发散式变化)
如果某个类经常因为不同的原因在不同的方向上发生变化,就会出现Divergent Change,如增加一个功能需要修改多处,这时应该把针对某一外界变化的所有相应修改都放在一个类中
Shotgun Surgery(霰弹式修改)
如果每遇到某种变化,就必须在许多不同的类内做出许多小修改,这就是Shotgun Surgery,应该把需要修改的代码放进同一个类,如果没有合适的类就创建一个
Feature Envy(依恋情结)
如果一个函数为了计算某个值,需要用到几个类的数据,就把函数移到最多被此函数使用的数据的类中
Data Clumps(数据泥团)
总是绑在一起出现的数据应该拥有属于它们自己的对象,评判方法是:删掉众多数据中的一项,如果其他数据没有意义了,那就应该为它们产生一个新对象
Primitive Obsession(基本类型偏执)
大多数编程环境都有两种数据:结构类型允许你将数据组织成有意义的形式;基本类型则是构成结构类型的积木块。结构总是会带来一定的额外开销,它们可能代表着数据库中的表,如果只为做一两件事而创建结构类型也可能显得太麻烦
对象技术的新手通常不愿意在小任务上运用小对象,但是对象的一个极大的价值在于:它们模糊(甚至打破)了横亘于基本数据和体积较大的类之间的界限
Switch Statements(switch惊悚现身)
面向对象程序的一个最明显特征就是:少用switch(或case)语句,从本质上来说,switch语句的问题在于重复,如果要为它添加一个新的case子句,就必须找到所有switch语句并修改它们,面向对象中的多态概念可为此带来优雅的解决方法
Parallel Inheritance Hierarchies(平行继承体系)
每当你为某个类增加一个子类,必须也为另一个类相应增加一个子类,这就是平行继承体系,消除这种重复性的一般策略是:让一个继承体系的实例引用另一个继承体系的实例
Lazy Class(冗赘类)
如果一个类的所得不值其身价,它就应该消失
Speculative Generality(夸夸其谈未来性)
当有人说”我想我们总有一天需要做这事”,并因而企图以各式各样的钩子和特殊情况来处理一些非必要的事情,这种坏味道就出现了,这么做的结果往往造成系统更难理解和维护,如果所有装置都会被用到,那就值得这么做;如果用不到,就应该移除
Temporary Field(令人迷惑的暂时字段)
其内某个实例变量仅为某种特定情况而设,这样的代码让人不易理解,因为通常会被认为对象在所有时候都需要它的所有变量,应该把所有和这个实例变量相关的代码都放进一个地方(或一个独立的类中)
Message Chains(过度耦合的消息链)
如果一个用户向一个对象请求另一个对象,然后再向后者请求另一个对象,然后再请求另一个对象…这就是消息链。采取这种方式,意味客户代码将与查找过程中的导航结构紧密耦合,一旦对象间的关系发生任何变化,客户端就不得不做出相应修改
可以先观察消息链最终得到的对象是用来干什么的,看看能否把使用该对象的代码提炼到一个独立的函数中,再把这个函数推入消息链。如果这条链上的某个对象有多位客户打算航行此航线的剩余部分,就加一个函数来做这件事
Middle Man(中间人)
对象的基本特性之一就是封装——对外部世界隐藏其内部细节,封装往往伴随委托
如果某个类接口有一半的函数都委托给其他类,就是过度运用委托,这时应该移除Middle Man,直接和真正负责的对象打交道。如果这样”不干实事”的函数只有少数几个,可以把它们放进调用端。如果这些Middle Man还有其他行为,可以把它变成负责对象的子类,这样既可以扩展原对象的行为,又不必负担那么多的委托动作
Inappropriate Intimacy(狎昵关系)
如果两个类过于亲密,需要花费太多时间去探究彼此的private成分,就是过分狎昵,可以把两个类的共同点提炼到一个新类中
继承往往造成过度亲密,因为子类对超类的了解总是超过后者的主观愿望
Incomplete Library Class(不完美的库类)
如果只想修改库类的一两个函数,可以运用Introduce Foreign Method;如果想要添加一大堆额外行为,就得运用Introduce Local Extension
Data Class(纯稚的数据类)
所谓Data Class是指:它们拥有一些字段,以及用于访问(读写)这些字段函数,除此之外一无长物,这样的类只是一种不会说话的数据容器,它们几乎一定被其他类过分细琐地操控着。需要找出这些取值/设值函数被其他类运用的地点,尝试把那些调用行为搬移到Data Class类,如果无法搬移整个函数,就产生一个可被搬移的函数
构筑测试体系
- 确保所有测试都完全自动化,让它们检查自己的测试结果
- 一套测试就是一个强大的bug侦测器,能够大大缩减查找bug所需要的时间
- 撰写测试代码的最有用时机是在开始编程之前,当你需要添加特性的时候,先写相应测试代码
- 频繁地运行测试,每次编译请把测试也考虑进去——每天至少执行每个测试一次
- 每当你收到bug报告,请先写一个单元测试来暴露bug
- 编写未臻完善的测试并实际运行,好过对完美测试的无尽等待
- 考虑可能出错的边界条件,把测试火力集中在那儿
- 测试时,别忘了检查预期的错误是否如期出现
- 当测试数量达到一定程度后,继续增加测试带来的效益就会呈现递减态势,而非持续递增,所以应该把测试集中在可能出错的地方
- 不要因为无法捕捉所有bug就不写测试,因为测试的确可以捕捉到大多数bug
重构列表
重构的记录格式
- 名称
- 简短概要:简单介绍此一重构手法的适用情景,以及它所做的事情
- 动机:”为什么需要这个重构”和”什么情况下不该使用这个重构”
- 做法:简明扼要地一步一步介绍如何进行此一重构
- 范例:以一个十分简单的例子说明此重构手法如何运作
Extract Method(提炼函数)
将一段代码放进一个独立函数中,并让函数名称解释该函数的用途
void printOwing(double amount){ printBanner(); //print details System.out.println("name:" + _name); System.out.println("amount" + amount); } => void printOwing(double amount){ printBanner(); printDetails(amount); } void printDetails(double amount){ System.out.println("name:" + _name); System.out.println("amount" + amount); }
动机
简短而命名良好的函数有以下几个优点:
- 如果每个函数的粒度都很小,那么函数被复用的机会就更大
- 会使高层函数读起来就像一系列注释
- 如果函数都是细粒度,那么函数的覆写也会更容易些
做法
- 创建一个新函数,根据这个函数的意图来对它命名(“做什么”)
- 将提炼出的代码从源函数复制到新建的目标函数中
- 仔细检查提炼出的代码,看看其中是否引用了”作用域限于源函数”的变量(包括局部变量和源函数参数)
- 检查是否有”仅用于被提炼代码段”的临时变量,如果有,在目标函数中将它们声明为临时变量
- 检查被提炼代码段,看看是否有任何局部变量的值被它改变。如果一个临时变量值被修改了,看看是否可以将被提炼代码段处理为一个查询,并将结果赋值给相关变量,如果很难这样做,或如果被修改的变量不止一个,就不能仅仅将这段代码原封不动地提炼出来
- 将被提炼代码段中需要读取的局部变量,当作参数传给目标函数
- 处理完所有局部变量之后,进行编译
- 在源函数中, 将被提炼代码段替换为对目标函数的调用
范例:无局部变量
void printOwing(){ Enumeration e = _orders.elements(); double outstanding = 0.0; // print banner System.out.println("***********************"); System.out.println("** Customer Owes**"); System.out.println("***********************"); // calculate outstanding while(e.hasMoreElements()){ Order each = (Order)e.nextElement(); outstanding += each.getAmount(); } // print details System.out.println("name:" + _name); System.out.println("amount" + outstanding); } => void printOwing(){ Enumeration e = _orders.elements(); double outstanding = 0.0; printBanner(); // calculate outstanding while(e.hasMoreElements()){ Order each = (Order)e.nextElement(); outstanding += each.getAmount(); } // print details System.out.println("name:" + _name); System.out.println("amount" + outstanding); } void printBanner(){ // print banner System.out.println("***********************"); System.out.println("** Customer Owes**"); System.out.println("***********************"); }
范例:有局部变量
void printOwing(){ Enumeration e = _orders.elements(); double outstanding = 0.0; printBanner(); // calculate outstanding while(e.hasMoreElements()){ Order each = (Order)e.nextElement(); outstanding += each.getAmount(); } // print details System.out.println("name:" + _name); System.out.println("amount" + outstanding); } => void printOwing(){ Enumeration e = _orders.elements(); double outstanding = 0.0; printBanner(); // calculate outstanding while(e.hasMoreElements()){ Order each = (Order)e.nextElement(); outstanding += each.getAmount(); } printDetails(outstanding); } void printDetails(double outstanding){ System.out.println("name:" + _name); System.out.println("amount" + outstanding); }
范例:对局部变量再赋值
void printOwing(){ Enumeration e = _orders.elements(); double outstanding = 0.0; printBanner(); // calculate outstanding while(e.hasMoreElements()){ Order each = (Order)e.nextElement(); outstanding += each.getAmount(); } printDetails(outstanding); } => void printOwing(){ printBanner(); double outstanding = getOutstanding(); printDetails(outstanding); } double getOutstanding(){ Enumeration e = _orders.elements(); double result = 0.0; while(e.hasMoreElements()){ Order each = (Order)e.nextElement(); result += each.getAmount(); } return result; }
如果需要返回的变量不止一个,最好的选择通常是:挑选另一块代码来提炼
Inline Method(内联函数)
在函数调用点插入函数本体,然后移除该函数
int getRating(){ return (moreThanFiveLateDeliveries()) ? 2 : 1; } boolean moreThanFiveLateDeliveries(){ return _numberOfLateDeliveries > 5; } => int getRating(){ return (_numberOfLateDeliveries > 5) ? 2 : 1; }
动机
如果使用了太多间接层,使得系统中的所有函数都似乎只是对另一个函数的简单委托,那就应该去除无用的间接层
做法
- 检查函数,确定它不具多态性,如果子类继承了这个函数,就不要将此函数内联,因为子类无法覆写一个根本不存在的函数
- 找出这个函数的所有被调用点
- 将这个函数的所有被调用点都替换为函数本体
- 编译、测试
- 删除该函数的定义
Inline Temp(内联临时变量)
将所有对该变量的引用动作,替换为对它赋值的那个表达式自身
double basePrice = anOrder.basePrice(); return (basePrice > 100) => return (anOrder.basePrice() > 100)
动机
Inline Temp多半是作为Replace Temp with Query的一部分使用,唯一单独使用的情况是:发现某个临时变量被赋予某个函数调用的返回值
做法
- 检查给临时变量赋值的语句,确保等号右边的表达式没有副作用
- 如果这个临时变量并未被声明为final,那就将它声明为final,然后编译,这可以检查该临时变量是否真的只被赋值一次
- 找到该临时变量的所有引用点,将它们替换为”为临时变量赋值”的表达式
- 每次修改后,编译并测试
- 修改完所有引用点后,删除该变量的声明和赋值语句
- 编译,测试
Replace Temp with Query(以查询取代临时变量)
将这个表达式提炼到一个独立函数中,将这个临时变量的所有引用点替换为对新函数的调用,此后,新函数就可被其他函数使用
double basePrice = _quantity * _itemPrice; if (basePrice > 1000) return basePrice * 0.95; else return basePrice * 0.98; => if (basePrice() > 1000) return basePrice() * 0.95; else return basePrice() * 0.98; ... double basePrice(){ return _quantity * _itemPrice; }
动机
临时变量的问题在于:它们是暂时的,而且只能在所属函数内使用,如果把临时变量替换为一个查询,那么同一个类中的所有函数都将可以获得这份信息
Replace Temp with Query往往是运用Extract Method之前必不可少的一个步骤,局部变量会使代码难以被提炼,所以应该尽可能把它们替换为查询式
做法
- 找出只被赋值一次的临时变量,超过一次考虑使用Split Temporary Variable将它分割成多个变量
- 将该临时变量声明为final
- 编译
- 将”对该临时变量赋值”之语句的等号右侧部分提炼到一个独立函数中
- 编译,测试
- 在该临时变量身上实施Inline Temp
范例
double getPrice(){ int basePrice = _quantity * _itemPrice; double discountFactor; if (basePrice > 1000) discountFactor = 0.95; else discountFactor = 0.98; return basePrice * discountFactor; } => double getPrice(){ return basePrice() * discountFactor(); } private double discountFactor(){ if (basePrice() > 1000) return 0.95; else return 0.98; } private int basePrice(){ return _quantity * _itemPrice; }
Introduce Explaining Variable(引入解释性变量)
将该复杂表达式(或其中一部分)的结果放进一个临时变量,以此变量名称来解释表达式用途
if ((platform.toUpperCase().indexOf("MAC") > -1) && (brower.toUpperCase().indexOf("IE") > -1) && wasInitialized() && resize > 0) { // do something } => final boolean isMacOs = platform.toUpperCase().indexOf("MAC") > -1; final boolean isIEBrowser = brower.toUpperCase().indexOf("IE") > -1; final boolean wasResized = resize > 0; if (isMacOs && isIEBrowser && wasInitialized() && wasResized){ // do something }
动机
表达式有可能非常复杂而难以阅读,这种情况下,临时变量可以帮助将表达式分解为比较容易管理的形式。使用这项重构的另一种情况是,在较长算法中,可以运用临时变量来解释每一步运算的意义
做法
- 声明一个final临时变量,将待分解之复杂表达式中的一部分动作的运算结果赋值给它
- 将表达式中的”运算结果”这一部分,替换为上述临时变量
- 编译,测试
- 重复上述过程,处理表达式的其他部分
范例
double price(){ // price is base price - quantity discount + shipping return _quantity * _itemPrice - Math.max(0, _quantity - 500) * _itemPrice * 0.05 + Math.min(_quantity * _itemPrice * 0.1, 100.0); } => double price(){ return basePrice() - quantityDiscount() + shipping(); } private double quantityDiscount(){ return Math.max(0, _quantity - 500) * _itemPrice * 0.05; } private double shipping(){ return Math.min(basePrice() * 0.1, 100.0); } private double basePrice(){ return _quantity * _itemPrice; }
Split Temporary Variable(分解临时变量)
针对每次赋值,创造一个独立、对应的临时变量
double temp = 2 * (_height + _width); System.out.println(temp); temp = _height * _width; System.out.println(temp); => final double perimeter = 2 * (_height + _width); System.out.println(temp); final double area = _height * _width; System.out.println(area);
动机
很多临时变量用于保存一段冗长代码的运算结果,以便稍后使用,这种临时变量应该只被赋值一次,如果它们被赋值超过一次,就意味着它们在函数中承担了一个以上的责任,如果临时变量承担多个责任,就应该被替换(分解)为多个临时变量,每个变量只承担一个责任
做法
- 在待分解临时变量的声明及其第一次被赋值处,修改其名称
- 将新的临时变量声明为final
- 以该临时变量的第二次赋值动作为界,修改此前对该临时变量的所有引用点,让它们引用新的临时变量
- 在第二次赋值处,重新声明原先那个临时变量
- 编译,测试
- 逐次重复上述过程,每次都在声明处对临时变量改名,并修改下次赋值之前的引用点
范例
double getDistanceTravellled(int time){ double result; double acc = _primaryForce / _mass; int primaryTime = Math.min(time,_delay); result = 0.5 * acc * primaryTime * primaryTime; int secondaryTime = time - _delay; if (secondaryTime > 0){ double primaryVel = acc * _delay; acc = (_primaryForce + _secondaryForce) / _mass; result += primaryVel * secondaryTime + 0.5 * acc * secondaryTime * secondaryTime; } return result; } => double getDistanceTravellled(int time){ double result; final double primaryAcc = _primaryForce / _mass; int primaryTime = Math.min(time,_delay); result = 0.5 * acc * primaryTime * primaryTime; int secondaryTime = time - _delay; if (secondaryTime > 0){ double primaryVel = primaryAcc * _delay; final double secondaryAcc = (_primaryForce + _secondaryForce) / _mass; result += primaryVel * secondaryTime + 0.5 * secondaryAcc * secondaryTime * secondaryTime; } return result; }
Remove Assignments to Parameters(移除对参数的赋值)
以一个临时变量取代该参数的位置
int discount(int inputVal, int quantity, int yearToDate){ if (inputVal > 50) inputVal -= 2; => int discount(int inputVal, int quantity, int yearToDate){ int result = inputVal; if (inputVal > 50) result -= 2;
动机
如果把一个名为foo的对象作为参数传给某个函数,那么”对参数赋值”意味改变foo,使它引用另一个对象,这会降低代码的清晰度,而且混用了按值传递和按引用传递两种参数传递方式。如果在”被传入对象”身上进行操作就没有问题
做法
- 建立一个临时变量,把待处理的参数值赋予它
- 以”对参数的赋值”为界,将其后所有对比参数的引用点,全部替换为”对此临时变量的引用”
- 修改赋值语句,使其改为对新建之临时变量赋值
- 编译、测试
范例
int discount(int inputVal, int quantity, int yearToDate){ if (inputVal > 50) inputVal -= 2; if (quantity > 100) inputVal -= 1; if (yearToDate > 10000) inputVal -= 4; return inputVal; } => int discount(final int inputVal, final int quantity, final int yearToDate){ int result = inputVal; if (inputVal > 50) result -= 2; if (quantity > 100) result -= 1; if (yearToDate > 10000) result -= 4; return result; }
Replace Method with Method Object(以函数对象取代函数)
如果有一个大型函数,可以将这个函数放进一个单独对象中,如此一来局部变量就成了对象内的字段,然后就可以在同一个对象中将这个大型函数分解为多个小型函数
动机
局部变量的存在会增加函数分解难度,Replace Method with Method Object会将所有局部变量都变成函数对象的字段,然后就可以将原本的大型函数拆解变短
做法
- 建立一个新类,根据待处理函数的用途,为这个类命名
- 在新类中建立一个final字段,用以保存原先大型函数所在的对象我们将这个字段称为”源对象”,同时,针对原函数的每个临时变量和每个参数,在新类中建立一个对应的字段保存
- 在新类中建立一个构造函数,接收源对象及原函数的所有参数作为参数
- 在新类中建立一个compute()函数
- 将原函数的代码复制到compute()函数中,如果需要调用源对象的任何函数,请通过源对象字段调用
- 编译
- 将旧函数的函数本体替换为这样一条语句”创建上述新类的一个新对象,而后调用其中的compute()函数”
范例
Class Account int gamma(int inputVal, int quantity, int yearToDate){ int importantValue1 = (inputVal * quantity) + delta(); int importantValue2 = (inputVal * yearToDate) + 100; if ((yearToDate - importantValue1) > 100) importantValue2 -= 20; int importantValue3 = importantValue2 * 7; // and so on. return importantValue3 - 2 * importantValue1; } => class Gamma... private final Account _account; private int inputVal; private int quantity; private int yearToDate; private int importantValue1; private int importantValue2; private int importantValue3; Gamma(Account source, int inputValArg, int quantityArg, int yearToDateArg){ _account = source; inputVal = inputValArg; quantity = quantityArg; yearToDate = yearToDateArg; } int compute(){ importantValue1 = (inputVal * quantity) + _account.delta(); importantValue2 = (inputVal * yearToDate) + 100; importantThing(); int importantValue3 = importantValue2 * 7; // and so on. return importantValue3 - 2 * importantValue1; } void importantThing(){ if ((yearToDate - importantValue1) > 100) importantValue2 -= 20; } class Account int gamma(int inputVal, int quantity, int yearToDate){ return new Gamma(this, inputVal, quantity, yearToDate).compute(); }
Substitute Algorithm(替换算法)
将函数本体替换为另一个算法
String foundPerson(String[] people){ for(int i = 0;i < people.length;i++){ if(people[i].equals("Don")){ return "Don"; } if(people[i].equals("John")){ return "John"; } if(people[i].equals("Kent")){ return "Kent"; } } return ""; } => String foundPerson(String[] people){ List candidates = Arrays.asList(new String[]{"Don", "John", "Kent"}); for(int i = 0; i < people.length; i++){ if(candidates.contants(people[i])){ return people[i]; } return ""; } }
动机
如果发现做一件事可以有更清晰的方式,就应该以较清晰的方式取代复杂的方式
做法
- 准备好另一个(替换用)算法,让它通过编译
- 针对现有测试,执行上述的新算法,如果结果与原本结果相同,重构结束
- 如果测试结果不同于原先,在测试和调试过程中,以旧算法为比较参照标准
- 重构-改善既有代码设计学习笔记(一)—代码的坏味道
- 《重构 改善既有代码的设计》——学习笔记(一)
- 学习《重构-改善既有代码的设计》一
- 《重构 改善既有代码的设计》学习笔记 1
- 重构 改善既有代码的设计-学习笔记
- 阅读《重构 改善既有代码的设计》学习笔记
- 《重构 改善既有代码的设计》——学习笔记(二)
- 《重构 改善既有代码的设计》——学习笔记(三)
- 《重构——改善既有代码的设计》
- 《重构——改善既有代码的设计》
- 阅读:重构——改善既有代码的设计
- 重构——改善既有代码的设计
- 重构 改善既有的代码设计——读书笔记
- 重构——改善既有代码的设计 读书笔记
- 《重构——改善既有代码的设计》读书笔记
- 《重构—改善既有代码的设计》 书籍小贴士
- 《重构—改善既有代码的设计》要点
- 重构—改善既有代码的设计
- 图像处理经典算子理解
- Android 消息机制源码分析
- Theano: CNMeM is disabled, CuDNN not available
- Oracle 笔记(一)体系结构
- java-笔记
- 《重构 改善既有代码的设计》——学习笔记(一)
- 简单了解osgi
- Activity和Fragment生命周期中的大坑------------
- JavaSE_8系列博客——基础篇(二)——从Hello World 开始说起
- CMAKE的使用
- javaSE_8系列博客——基础篇(一)--让我们开始吧!
- word2vec 中的数学原理详解
- [七]RabbitMQ-客户端源码之AMQPImpl+Method
- 2016河南省第九届ACM程序设计竞赛,问题 E: 机器设备