《重构-改善既有代码的设计》学习笔记(二)
昨天讲了那么多坏味道,今天就说下该怎样清理那些坏味道。
一.重新组织函数
1.提炼函数(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); }
动机:简短而命名良好的函数有以下几个优点:
- 如果每个函数的粒度都很小,那么函数被复用的机会就更大
- 会使高层函数读起来就像一系列注释
- 如果函数都是细粒度,那么函数的覆写也会更容易些
注意:创建函数时,根据这个函数的意图来对它命名(以它“做什么”来命名,而不是以它“怎样做”命名)
2.内联函数(Inline Method)
在函数调用点插入函数本体,然后移除该函数。
int getRating(){ return (moreThanFiveLateDeliveries()) ? 2 : 1; } boolean moreThanFiveLateDeliveries(){ return _numberOfLateDeliveries > 5; }
↓↓↓↓↓↓↓↓替换
int getRating(){ return (_numberOfLateDeliveries > 5) ? 2 : 1; }
动机:当你遇到某些函数,其内部代码和函数名称同样清晰易读时,就可以试试“内联函数”。
如果使用了太多间接层,使得系统中的所有函数都似乎只是对另一个函数的简单委托,那就应该去除无用的间接层
3.内联临时变量(Inline Temp)
将所有对该变量的引用动作,替换为对它赋值的那个表达式自身。
double basePrice = anOrder.basePrice(); return (basePrice > 100) ↓↓↓↓↓↓↓↓替换 return (anOrder.basePrice() > 100)
动机:Inline Temp多半是作为Replace Temp with Query的一部分使用,唯一单独使用的情况是:发现某个临时变量被赋予某个函数调用的返回值。
4.以查询取代临时变量(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之前必不可少的一个步骤,局部变量会使代码难以被提炼,所以应该尽可能把它们替换为查询式
5.引入解释性变量(Introduce Explaning 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 }
动机:表达式有可能非常复杂而难以阅读,这种情况下,临时变量可以帮助将表达式分解为比较容易管理的形式。使用这项重构的另一种情况是,在较长算法中,可以运用临时变量来解释每一步运算的意义
6.分解临时变量(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);
动机:很多临时变量用于保存一段冗长代码的运算结果,以便稍后使用,这种临时变量应该只被赋值一次,如果它们被赋值超过一次,就意味着它们在函数中承担了一个以上的责任
,如果临时变量承担多个责任,就应该被替换(分解)为多个临时变量,每个变量只承担一个责任
7.移除对参数的赋值(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,使它引用另一个对象,这会降低代码的清晰度,而且混用了按值传递和按引用传递两种参数传递方式。如果在”被传入对象”身上进行操作就没有问题
8.以函数对象取代函数(Replace Method with Method Object)
如果有一个大型函数,可以将这个函数放进一个单独对象中,如此一来局部变量就成了对象内的字段,然后就可以在同一个对象中将这个大型函数分解为多个小型函数
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(); }
动机:局部变量的存在会增加函数分解难度。如果一个函数之中局部变量泛滥成灾,那么想分解这个函数就非常困难。
9.替换算法(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.搬移函数(Move Method)
在该函数最常引用的类中建立一个有着类似行为的新函数,将旧函数变成一个单纯的委托函数,或是将旧函数完全移除
class Account... double overdraftCharge(){ if (_type.isPremium()){ double result = 10; if (_daysOverdrawn > 7) result += (_daysOverdrawn - 7) * 0.85; return result; } else return _dayOverdrawn * 1.75; } double bankCharge(){ double result = 4.5; if (_dayOverdrawn > 0) result += overdraftCharge(); return result; } private AccountType _type; private int _daysOverdrawn; ↓↓↓↓↓↓↓↓替换 class Account... double bankCharge(){ double result = 4.5; if (_dayOverdrawn > 0) result += _type.overdraftCharge(_daysOverdrawn); return result; } class AccountType... double overdraftCharge(Account account){ if (isPremium()){ double result = 10; if (account.getDaysOverdrawn > 7) result += (account.getDaysOverdrawn - 7) * 0.85; return result; } else return account.getDayOverdrawn * 1.75; }
动机:“搬移函数”是重构理论的支柱,如果一个类有太多行为,或如果一个类与另一个类有太多合作而形成高度耦合,就应该搬移函数,通过这种手段,可以使系统中的类更简单,这些类最终也将更干净利落地实现系统交付的任务
2.搬移字段(Move Field)
如果某个字段被其所驻类之外的另一个类更多地用到,就应该在目标类新建一个字段,修改源字段的所有用户,令它们改用新字段
class Account... private AccountType _type; private double _interestRate; double interestForAmountDays(double amount, int days){ return _interestRate * amount * days / 365; } ↓↓↓↓↓↓↓↓ 将_interestRate搬移到AccountType类中 class AccountType... private double _interestRate; void setInterestRate(double arg){ _interestRate = arg; } double getInterestRate(){ return _interestRate; } class Account... private AccountType _type; private double _interestRate; double interestForAmountDays(double amount, int days){ return _type.getInterestRate() * amount * days / 365; }
动机:对于一个字段,在其所驻类之外的另一个类中有更多函数使用了它,就应该考虑搬移这个字段
3.提炼类(Extract Class)
如果某个类做了应该由两个类做的事,就应该建立一个新类,将相关的字段和函数从旧类搬移到新类
class Person... public String getName(){ return _name; } public String getTelephoneNumber(){ return ("(" + _officeAreaCode + ") " + _officeNumber); } String getOfficeAreaCode(){ return _officeAreaCode; } void setOfficeAreaCode(String arg){ _officeAreaCode = arg; } String getOfficeNumber(){ return _officeNumber; } void setOfficeNumber(String arg){ _officeNumber = arg; } private String _name; private String _officeAreaCode; private String _officeNumber; ↓↓↓↓↓↓↓↓ 新建一个TelephoneNumber类 class Person... public String getName(){ return _name; } public String getTelephoneNumber(){ return _officeTelephone.getTelephoneNumber(); } TelephoneNumber getOfficeTelephone(){ return _officeTelephone; } private String _name; private TelephoneNumber _officeTelephone = new TelephoneNumber(); class TelephoneNumber... public String getTelephoneNumber(){ return ("(" + _areaCode + ") " + _number); } String getAreaCode(){ return _areaCode; } void setAreaCode(String arg){ _areaCode = arg; } String getNumber(){ return _number; } void setNumber(String arg){ _number = arg; } private String _number; private String _areaCode;
动机:一个类应该是一个清楚的抽象,处理一些明确的责任
4.将类内联化(Inline Class)
将这个类的所以特性搬移到另一个类中,然后移出原类。
动机:如果一个类不再承担足够责任、不再有单独存在的理由(这通常是因为此前的重构动作移走了这个类的责任),就应该挑选这一”萎缩类”的最频繁用户(也是个类),以Inline Class手法将”萎缩类”塞进另一个类中
5.隐藏“委托关系”(Hide Delegate)
客户通过一个委托类来调用另一个对象,在服务类上建立客户所需的所有函数,用以隐藏委托关系
class Person{ Department _department; public Department getDepartment(){ return _department; } public void setDepartment(Department arg){ _department = arg; } } class Department{ private String _chargeCode; private Person _manager; public Department(Person manager){ _manager = manager; } public Person getManager(){ return _manager; } ... } ↓↓↓↓↓↓↓↓ 对客户隐藏Department,在Person中增加 public Person getManager(){ return _department.getManager(); }
动机:对象的”封装”特性意味每个对象都应该尽可能少了解系统的其他部分,如果某个客户先通过服务对象的字段得到另一个对象,然后调用后者的函数,那么客户就必须知晓这一层委托关系
,应该在服务对象上放置一个简单的委托函数,将委托关系隐藏起来,从而去除这种依赖
6.移除中间人(Remove Middle Man)
如果某个类做了过多的简单委托动作,就让客户直接调用受托类
class Person... Department _department; public Person getManager(){ return _department.getManager(); } class Department... private Person _manager; public Department(Person manager){ _manager = manager; } ↓↓↓↓↓↓↓↓替换 class Person... public Department getDepartment(){ return _department; } manager = john.getDepartment().getManager();
动机:“封装受托对象”的代价是每当客户要使用受托类的新特性时,就必须在服务端添加一个简单委托函数,随着受托类的特性越来越多,服务类完全变成了一个”中间人”,此时就应该让客户直接调用受托类
7.引入外加函数(Introduce foreign Method)
在客户类中建立一个函数,并以第一参数形式传入一个服务类型实例。
Date newStart = new Date(previousEnd.getYear(), previousEnd.getMonth(), previousEnd.getDate() + 1); ↓↓↓↓↓↓↓↓替换 Date newStart = nextDay(previousEnd); private static Date nextDay(Date arg){ return New Date(arg.getYear(), arg.getMonth(), arg.getDate() + 1); }
动机:当需要服务类提供一个新功能时,又不能修改服务类的源码,就得在客户端编码,如果需要为服务类建立大量外加函数或有许多类都需要同样的外加函数,就不应该再使用本项重构,而应该使用Introduce Local Extensio
8.引入本地扩展(Introduce local Extension)
建立一个新类,使它包含这些额外函数。然这个扩展品成为源类的子类或包装类。
源类的子类:
client class... private static Date nextDay(Date arg){ // foreign method,should be on date return new Date(arg.getYear(), arg.getMonth(), arg.getDate() + 1); } ↓↓↓↓↓↓↓↓替换 // 用MfDateSub包装Date类 class MfDateSub... Date nextDay(){ return new Date(getYear(), getMonth(), getDate() + 1); }
源类的包装类:
client class... private static Date nextDay(Date arg){ // foreign method,should be on date return new Date(arg.getYear(), arg.getMonth(), arg.getDate() + 1); } ↓↓↓↓↓↓↓↓替换 class MfDateWrap... Date nextDay(){ return new Date(getYear(), getMonth(), getDate() + 1); } public int getYear(){ return _original.getYear(); } public boolean equals(Object arg){ if (this == arg) return true; if (!(arg instanceof MfDateWrap)) return false; MfDateWrap other = ((MfDateWrap) arg); return (_original.equals(other._original)); }
动机:
本地扩展是一个独立的类,但也是被扩展类的子类型:它提供源类的一切特性,同时额外添加新特性。在任何使用源类的地方,都可以使用本地扩展取而代之。
在子类和包装类之间做选择时,通常首选子类,因为这样的工作量比较少。制作子类的最大障碍在于,它必须在对象创建期实施,此外,子类化方案还必须产生一个子类对象
,这种情况下, 如果有其他对象引用了旧对象,就同时有两个对象保存了原数据,如果原数据允许被修改,就必须改用包装类,使用包装类时,对本地扩展的修改会波及原对象,反之亦然