0. 本章内容导图
在对象技术中,最重要的概念莫过于“接口”,容易被理解和被使用的接口是开发良好面向对象软件的关键。本章介绍的重构手法是用来使接口变得更简洁易用的。
简化函数调用1. 重构手法
1.1 函数改名
概要:
函数的名称未能揭示函数的用途。
修改函数名称。
动机:
a. 让函数名称准确表达它的用途
示例:
重构前:
public String getTelephoneNumber() {
return mOfficeAreaCode + "-" +mOfficeNumber;
}
重构后:
// 此方法是否要删除,需要根据它是否被客户代码所使用
public String getTelephoneNumber() {
return getOfficeTelephoneNumber();
}
public String getOfficeTelephoneNumber() {
return mOfficeAreaCode + "-" +mOfficeNumber;
}
总结:
将复杂的处理过程分解成小函数是一种良好的编程风格,如果函数命名的好,理解整个处理过程就像阅读一行行的注释一样,理想情况是函数的名称能像自然语言一样表达出自己的功能。如果看到一个函数名称不能很好地表达它的用途,应该立马加以修改。起个好名字是成为编程高手的必备技能。
给函数命名有一个好办法:首先考虑应该给这个函数写上一句怎样的注释,然后想办法将注释变成函数名称。
如果旧函数已被客户调用,可以新增一个命名良好的函数实现相同功能,让旧函数转调新函数。
1.2 添加参数
概要:
某个函数需要从调用端得到更多信息。
为此函数添加一个对象参数,让该对象带进函数所需信息。
动机:
a. 函数需要一些过去没有的信息,通过参数将所需信息传递进来
示例:
重构前:
getContact() {
// do something
}
重构后:
//需求变更或做其他重构,必须修改此函数,让它从对象参数获得某些信息
getContact(Date date) {
// do something
}
总结:
使用本重构手法时要仔细考虑是否一定需要添加参数,是否有其他的选择。
1.3 移除参数
概要:
函数本体不再需要某个参数。
将该参数去除。
动机:
a. 去除冗余,简化函数调用
示例:
getContact(Date date) {
// do something
}
重构后:
//需求变更或做了其他重构,已不再需要参数对象提供信息
getContact() {
// do something
}
总结:
参数代表着函数所需的信息,不同的参数有不同的意义,函数调用者需要考虑每一个调用所需要的参数,因此,如果已经不再需要某个参数了,要及时去除。
1.4 将查询函数和修改函数分离
概要:
某个函数既返回对象状态值,又修改对象状态。
建立两个不同的函数,其中一个负责查询,另一个负责修改。
动机:
a. 保持函数职责单一,避免函数调用的副作用
示例:
重构前:
Object getTotalOutstandingAndSetReadyForSummaries() {
}
重构后:
Object getTotalOutstanding() {
}
setReadyForSummaries() {
}
总结:
承担多个责任的函数一般较难命名,往往需要在命名时引入And/Or等,当你遇到这种命名难题时,考察一下函数是否既做了查询又做了修改。
每次调用查询函数同时又会修改对象某个状态值的话,很容易引起难以排查的bug。
1.5 令函数携带参数
概要:
若干函数做了类似的工作,但在函数本体中却包含了不同的值。
建立单一函数,以参数表达那些不同的值。
动机:
a. 通过参数处理变化的情况,简化问题,去除重复代码
示例:
重构前:
void tenPercentRaise() {
salary *= 1.1;
}
void fivePercentRaise() {
salary *= 1.05;
}
重构后:
void raise(double factor) {
salary *= (1 + factor);
}
总结:
有时候这种方法并不能处理整个函数,但可以处理函数中的一部分代码,即便如此,也应将这部分代码提炼到一个独立函数中,用函数调用去除重复的代码。
1.6 以明确函数取代参数
概要:
你有一个函数,其中完全取决于参数值而采取不同行为。
针对该参数的每一个可能值,建立一个独立函数。
动机:
a. 获得更清晰的接口
示例:
重构前:
void setValue(String name, int value) {
if (name.equals("height")) {
mHeight = value;
return;
}
if(name.equals("width")) {
mWidth = value;
return;
}
Assert.shouldNeverReachHere();
}
重构后:
void setHeight(int height) {
mHeight = height;
}
void setWidth(int width) {
mWidth = width;
}
总结:
如果某个参数有多种可能的值,而函数内又以条件表达式检查这些参数值,并根据不同参数做出不同的行为,就应该使用本项重构。重构后不仅可以使接口更清晰,还避免了对参数值进行合法性检测的步骤。
1.7 保持对象完整
概要:
你从某个对象中取出若干值,将它们作为某一次函数调用时的参数。
改为传递整个对象。
动机:
a. 预防被调用函数将来需要新的数据项
b. 避免过长的参数列
示例:
重构前:
int low = daysTempRange().getLow();
int high = daysTempRange().getHigh();
withinPlan = plan.withinRange(low, high);
重构后:
withinPlan = plan.withinRange(daysTempRange());
总结:
当把对象当作参数传递给函数时,被调用函数所在的对象就需要依赖此参数对象,要谨防依赖结构恶化。
如果被调用函数使用来自另一个对象的很多项数据,要考虑这个函数是否应该定义在数据所属的对象中。
1.8 以函数取代参数
概要:
对象调用某个函数,并将所得结果作为参数,传递给另一个函数。
而接受该参数的函数本身也能够调用前一个函数。
让参数接受者去除该项参数,并直接调用前一个函数。
动机:
a. 函数可以通过其他途径获得参数值,就不应该通过参数取得该值
示例:
重构前:
int basePrice = mQuantity * mItemPrice;
discountLevel = getDiscountLevel();
double finalPrice = discountedPrice(basePrice, discountLevel);
重构后:
int basePrice = mQuantity * mItemPrice;
double finalPrice = discountedPrice(basePrice);
总结:
过长的参数列会增加程序阅读者的理解难度,应该尽可能地缩短参数列长度。如果所传参数本身可以被函数直接调用到,就没有必要再通过参数来传递了。
1.9 引入参数对象
概要:
某些参数总是很自然地同时出现。
以一个对象取代这些参数。
动机:
a. 缩短参数列
b. 通过参数对象使代码对数据的访问更具一致性
示例:
重构前:
Bill getBill(Date start, Date end) {
//统计参数日期范围内的账单并返回
}
重构后:
class DateRange {
private final Date mStart;
private final Date mEnd;
public DateRange(Date start, Date end) {
mStart = start;
mEnd = end;
}
public Date getStart() {
return mStart;
}
public Date getEnd() {
return mEnd;
}
}
Bill getBill(DateRange dateRange) {
//统计参数日期范围内的账单并返回
}
总结:
要留意这组参数是否总是在多个地方被一起传递。另外,把这些参数组织到一起之后,就会发现可以将一些行为移至新建的类中,还可减少很多重复代码。
1.10 移除设值函数
概要:
类中的某个字段应该在对象创建时被设值,然后就不再改变。
去掉该字段的所有设值函数。
动机:
a. 使字段不可被修改的意图更清晰
b. 排除字段被修改的可能性
示例:
重构前:
class Account {
private String mId;
Account(String id) {
mId = id;
}
void setId(String id) {
mId = id;
}
}
重构后:
class Account {
private final String mId;
Account(String id) {
mId = id;
}
}
总结:
如果不想用户修改,就不应该提供可以修改的函数,否则,是无法知晓用户会怎样使用的。另外,为了清晰地表达这层意图,应该通过一些语法修饰(如用final修饰)来明确这种意图。
1.11 隐藏函数
概要:
有一个函数,从来没有被其他任何类用到。
将这个函数修改为private。
动机:
a. 降低函数可见度
示例:
重构前:
class Employee {
public void unusedMethodByOtherClass() {
}
}
重构后:
class Employee {
private void unusedMethodByOtherClass() {
}
}
总结:
将未被其他类用到的方法封装起来,还可以明确地告诉代码阅读者此方法是在类内部使用的,不属于类对外提供的服务接口。
1.12 以工厂函数取代构造函数
概要:
你希望在创建对象时不仅仅是做简单的建构动作。
将构造函数替换为工厂函数。
动机:
a. 在派生子类的过程中以工厂函数取代类型码
示例:
重构前:
class Person {}
class Male extends Person {}
class Female extends Person {}
Person jack = new Male();
重构后:
class Person {
static Person createMale() {
return new Male();
}
static Person createFemale() {
return new Female();
}
}
class Male extends Person {}
class Female extends Person {}
Person jack = Person.createMale();
总结:
根据使用的场景,你还可以使用简单工厂模式或工厂方法模式来解决这类问题。
《Effective Java》一书中,Joshua Bloch介绍的第一条经验法则就是:考虑用静态工厂方法代替构造器,并介绍了这种方法的优点和缺点,以及在Java Collections Framework中的运用。
1.13 封装向下转型
概要:
某个函数返回的对象,需要由函数调用者执行向下转型。
将向下转型动作移到函数中。
动机:
a. 尽量给用户提供准确的类型,减少用户非必要的工作
示例:
重构前:
Object lastReading() {
return readings.lastElement();
}
重构后:
Reading lastReading() {
return (Reading)readings.lastElement();
}
总结:
这种情况常出现返回迭代器或集合的函数身上,Java5引入泛型之后,实际上做了很多类似的工作,可以参看Java集合源码。
1.14 以异常取代错误码
概要:
某个函数返回一个特定的代码,用以表示某种错误情况。
改用异常。
动机:
a. 将“普通程序”和“错误处理”区分开,使程序更容易理解
示例:
重构前:
int withdraw(int amount) {
if (amount > mBalance) {
return -1;
} else {
mBalance -= amount;
return 0;
}
}
重构后:
void withdraw(int amount) throws BalanceException {
if (amount > mBalance) {
throw new BalanceException();
}
mBalance -= amount;
}
总结:
程序中发现错误的地方并不一定知道如何处理错误,当一段子程序发现错误时,它需要让它的调用者知道这个错误,而调用者也可能将这个错误继续沿着调用链传递上去。Unix系统和基于C语言的软件是以返回值来表示子程序的成功或失败。Java语言引入了异常这种错误处理机制,可以使得你写出更健壮、清晰的代码。
1.15 以测试取代异常
概要:
面对一个调用者可以预先检查的条件,你抛出了一个异常。
修改调用者,使它在调用函数之前先做检查。
动机:
a. 避免滥用异常
示例:
重构前:
double getValueForPeriod(int periodNumber) {
try {
return mValues[periodNumber];
} catch (ArrayIndexOutOfBoundsException e) {
return 0;
}
}
重构后:
double getValueForPeriod(int periodNumber) {
if (periodNumber >= mValues.length) {
return 0;
}
return mValues[periodNumber];
}
总结:
异常的出现是程序语言的一大进步,但异常也不应该被滥用,它只应该被用于异常的、罕见的行为,指的是那些会产生意料之外的错误的行为,异常不应该成为条件检查的替代品,当可以对输入做预先检查时,就先对其进行取值检查,而不是任其产生异常,将错误抛出。
网友评论