重构速查表

重构的相关技能
帅旋
关注
充电
IT宅站长,技术博主,共享单车手,全网id:arthinking。

简化函数调用

发布于 2019-03-08 | 更新于 2024-02-23

函数改名

如果一个函数未能揭示函数的用途,那么请通过 Rename Method(函数改名) 修改函数名称。

1
function circum(radius) {...}
1
function circumference(radius) {...}

好处

  • 增加代码可读性。尝试为新方法指定一个反映其功能的名称。像createOrder()renderCustomerInfo()等。

添加参数

假设某个函数需要从欧冠调用端得到更多信息,那么可以尝试通过 Add Parameter(添加参数) 为此函数添加一个对象参数,让该对象带进函数所需信息。

1
function getContact() {...}
1
function getContact(var date) {...}

注意

除了添加参数外,你常常还有其他选择,只要可能,都比添加参数要好,因为它们不会增加参数列表长度。过长参数往往伴随着坏味道Data Clumps

可选做法:

  • 看看可否从这些参数得到所需信息;
  • 看看有没有可能通过调用某个函数提供 所需信息;
  • 这个函数是否应该属于拥有该信息的那个对象所有;
  • 是否可以考虑使用 Introduct Parameter Object

好处

  • 您可以选择添加新参数或者添加包含方法所需数据的新私有字段。 当您需要偶尔或经常更改的数据时,最好将字段保存在对象中。

移除参数

如果函数体不再需要某个参数了,请 Remove Parameter(移除参数)

1
function getContact(var date) {...}
1
function getContact() {...}

好处

  • 方法仅包含它真正需要的参数。

注意

如果方法在子类或超类中以不同方式实现,并且您的参数在这些实现中使用,请保持参数不变。

将查询函数和修改函数分离

如果某个函数既返回对象状态,有修改对象状态,那么通过 Separate Query from Modifier(将查询函数和修改函数分离) 建立两个不同的函数,其中一个负责查询,另一个负责修改,任何有返回值的函数,都不应该有看得到的副作用。

1
2
3
4
5
function getTotalOutstandingAndSendBill() {
const result = customer.invoices.reduce((total, each) => each.amount + total, 0);
sendBill();
return result;
}
1
2
3
4
5
6
function totalOutstanding() {
return customer.invoices.reduce((total, each) => each.amount + total, 0);
}
function sendBill() {
emailGateway.send(formatBill(customer));
}

获取数据的代码一般被命名为query

好处

  • 如果您的查询没有改变程序的状态,那么您可以根据需要多次调用它,而不必担心因调用该方法而导致意外更改数据;
  • 但请记住,仅在修改器改变对象的可见状态的情况下,副作用才是危险的。例如,这些可以是从对象的公共接口,数据库中的条目,文件等中访问的字段。如果修饰符仅缓存复杂的操作并将其保存在类的私有字段中,则几乎不会导致任何一方效果。

令函数携带参数

如果若干个函数做了类似的工作,但是在函数本地中却包含了不同的值,那么尝试使用 Parameterize Method(令函数携带参数) 建立单一函数,以参数表达那些不同的值。

1
2
3
4
5
6
function tenPercentRaise(aPerson) {
aPerson.salary = aPerson.salary.multiply(1.1);
}
function fivePercentRaise(aPerson) {
aPerson.salary = aPerson.salary.multiply(1.05);
}
1
2
3
4
5
6
function tenPercentRaise(aPerson) {
aPerson.salary = aPerson.salary.multiply(1.1);
}
function fivePercentRaise(aPerson) {
aPerson.salary = aPerson.salary.multiply(1.05);
}

注意

  • 有时这种重构技术可能会把你带偏,导致一个漫长而复杂的常用方法,而不是多个简单的方法;
  • 将功能的激活/取消激活移动通过参数标识的时候也要小心。这最终可能导致创建一个大的条件运算符,需要通过使用显式方法替换参数来处理。

以明确函数取代参数

如果你有一个函数,其中完全取决于不同的参数值而采取不同的行为,那么请通过 Replace Parameter with Explicit Method(以明确函数取代参数)

包含参数依赖变体的方法将会变得非常庞大。

1
2
3
4
5
6
7
8
9
10
11
void setValue(String name, int value) {
if (name.equals("height")) {
height = value;
return;
}
if (name.equals("width")) {
width = value;
return;
}
Assert.shouldNeverReachHere();
}
1
2
3
4
5
6
void setHeight(int arg) {
height = arg;
}
void setWidth(int arg) {
width = arg;
}

好处

  • 提高代码可读性。理解startEngine()的目的要比setValue("engineEnabled", true)容易得多。

保持对象完整

如果您从对象获取多个值,然后将它们作为参数传递给方法,请通过 Preserve Whole Object(保持对象完整) 改为传递整个对象。

1
2
3
int low = daysTempRange.getLow();
int high = daysTempRange.getHigh();
boolean withinPlan = plan.withinRange(low, high);
1
boolean withinPlan = plan.withinRange(daysTempRange);

注意

如果被调用函数使用了来自另一个对象的很对象数据,这可能意味着函数实际上应该被定义在那些数据所属的对象中,可以考虑使用 Preserve Whole Object 的同时,也考虑 Move Method 。如果此时还没有一个完整对象,可以先使用 Introduce Parameter Object(引入参数对象)

好处

  • 您可以看到一个具有易于理解的名称的对象,而不是大量的参数;
  • 如果方法需要来自对象的更多数据,则不需要重写使用该方法的所有的调用位置,仅在方法本身内部调整即可。

以函数调用取代参数

如果发现对象调用了某个函数,然后将结果作为参数传递给另一个函数,而接受参数的那个函数本身也可以调用前一个函数,那么,请使用 Replace Parameter with Method Call(以函数调用取代参数)

1
2
3
4
int basePrice = quantity * itemPrice;
double seasonDiscount = this.getSeasonalDiscount();
double fees = this.getFees();
double finalPrice = discountedPrice(basePrice, seasonDiscount, fees);
1
2
int basePrice = quantity * itemPrice;
double finalPrice = discountedPrice(basePrice);

好处

  • 我们摆脱了不需要的参数并简化了方法调用,这些参数通常不是像现在这样为项目创建的,而是着眼于未来可能永远不会出现的需求,你应该在必要关头才添加参数,预先添加的参数很可能并不是你所需要的。

例外:如果修改接口灰度整个程序造成非常痛苦的结果,那么可以考虑保留前人预先加入的参数。此时你也应该评估系统各部分之间的依赖,判断接口是否合理。

引入参数对象

某些参数总是很自然的同时存在,可以考虑使用 Introduce Parameter Object(引入参数对象) 以一个对象取代这些参数。

1
2
3
function amountInvoiced(startDate, endDate) {...}
function amountReceived(startDate, endDate) {...}
function amountOverdue(startDate, endDate) {...}
1
2
3
function amountInvoiced(startDate, endDate) {...}
function amountReceived(startDate, endDate) {...}
function amountOverdue(startDate, endDate) {...}

好处

  • 更易读的代码。您可以看到一个具有易于理解的名称的对象,而不是大量的参数;
  • 在各处分散的相同参数组(虽然没有调用相同的代码,但是经常遇到相同的参数和参数组)创建了它们自己的对象,从而避免了重复代码。

移除设值函数

如果类中的某个字段在对象创建的时候被设值,然后就不会在改变了,而您希望阻止对字段值的任何更改,那么请 Remove Setting Method(移除设值函数)

1
2
3
class Person {
get name() {...}
set name(aString) {...}
1
2
class Person {
get name() {...}

隐藏函数

如果有一个函数从来没有被其他任何类调用到,那么请使用 Hide Method(隐藏函数) 将这个函数修改为private。

好处

  • 隐藏方法使您的代码更容易发展。当您更改私有方法时,您只需要担心如何不破坏当前类,因为您知道该方法不会在其他任何地方使用;
  • 通过将方法设为私有,您强调了类的公共接口和保持公共的方法的重要性。

以工厂函数取代构造函数

如果你希望在创建对象的时候不仅仅是做简单的构建动作,那么请 Replace Constructor with Factory Method(以工厂函数取代构造函数)

1
2
3
4
5
6
class Employee {
Employee(int type) {
this.type = type;
}
// ...
}
1
2
3
4
5
6
7
8
class Employee {
static Employee create(int type) {
employee = new Employee(type);
// do some heavy lifting.
return employee;
}
// ...
}

使用 Replace Constructor with Factory Method(以工厂函数取代构造函数) 最显而易见的动机,就是在派生子类的过程中,以工厂函数取代类型码。

好处

  • 工厂方法不一定返回调用它的类的对象。通常这些可以是它的子类,根据给予该方法的参数进行选择;
  • 工厂方法可以有一个更好的名称来描述它返回的内容和方式,例如Troops :: GetCrew(myTank);
  • 工厂方法可以返回已创建的对象,与构造函数不同,后者始终创建新实例。

封装向下转型

如果某个函数返回的对象,需要由汉调用者执行向下转型,那么可以通过 Encapsulate Downcast(封装向下转型) 将向下转型动作移动到函数中。

1
2
3
Object lastReading() {
return readings.lastElement();
}
1
2
3
Reading lastReading() {
return (Reading) readings.lastElement();
}

以异常取代错误码

如果某个函数返错误码,那么请 Repalce Error Code with Exception(以异常取代错误码)

返回错误代码是程序编程中的一种过时的处理方法。在现代编程中,错误处理由特殊类执行,这些类被命名为exception。如果出现问题,则“抛出”一个错误,然后由一个异常处理程序“捕获”。

1
2
3
4
5
6
7
8
9
int withdraw(int amount) {
if (amount > _balance) {
return -1;
}
else {
balance -= amount;
return 0;
}
}
1
2
3
4
5
6
void withdraw(int amount) throws BalanceException {
if (amount > _balance) {
throw new BalanceException();
}
balance -= amount;
}

好处

  • 避免了在大量条件中书写各种错误代码检查。异常处理程序是将普通程序和错误处理区分开来的更为简洁的方法;
  • 异常类可以实现自己的方法,因此包含部分错误处理功能(例如用于发送错误消息);
  • 与异常不同,错误代码不能在构造函数中使用,因为构造函数必须只返回一个新对象。

以测试取代异常

如果可以预先检查的条件,却抛出了异常,请通过 Replace Exception with Test(以测试取代异常) 修改调用者,使它在调用函数之前先做检查。

1
2
3
4
5
6
7
double getValueForPeriod(int periodNumber) {
try {
return values[periodNumber];
} catch (ArrayIndexOutOfBoundsException e) {
return 0;
}
}
1
2
3
4
5
6
double getValueForPeriod(int periodNumber) {
if (periodNumber >= values.length) {
return 0;
}
return values[periodNumber];
}

注意

应使用异常来处理与意外错误相关的不规则行为。它们不应该作为测试的替代品。如果只需在运行前验证条件就可以避免异常,那么就这样做。应该为实际错误保留例外情况。

好处

  • 简单条件有时可能比异常处理代码更直白明了。

本文作者: 帅旋

本文链接: https://www.itzhai.com/columns/refactoring/simplified-function-call.html

版权声明: 版权归作者所有,未经许可不得转载,侵权必究!联系作者请加公众号。

×
IT宅

关注公众号及时获取网站内容更新。