0%
这是一片思考的空间 -- arthinking
Spring 重构&代码整洁之道 软件设计 JVM 并发编程 数据结构与算法 分布式 存储 网络 微服务 设计模式
Java技术栈 - 涉及Java技术体系

简化函数调用

函数改名

如果一个函数未能揭示函数的用途,那么请通过 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];
}

注意

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

好处

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

欢迎关注我的其它发布渠道

订阅IT宅
内功修炼
Java技术栈
Java架构杂谈是IT宅精品文章公众号,欢迎订阅:
📄 网络基础知识:两万字长文50+张趣图带你领悟网络编程的内功心法 📄 HTTP发展史:三万长文50+趣图带你领悟web编程的内功心法 📄 HTTP/1.1:可扩展,可靠性,请求应答,无状态,明文传输 📄 HTTP/1.1报文详解:Method,URI,URL,消息头,消息体,状态行 📄 HTTP常用请求头大揭秘 📄 HTTPS:网络安全攻坚战 📄 HTTP/2:网络安全传输的快车道 📄 HTTP/3:让传输效率再一次起飞 📄 高性能网络编程:图解Socket核心内幕以及五大IO模型 📄 高性能网络编程:三分钟短文快速了解信号驱动式IO 📄 高性能网络编程:彻底弄懂IO复用 - IO处理杀手锏,带您深入了解select,poll,epoll 📄 高性能网络编程:异步IO:新时代的IO处理利器 📄 高性能网络编程:网络编程范式 - 高性能服务器就这么回事 📄 高性能网络编程:性能追击 - 万字长文30+图揭秘8大主流服务器程序线程模型
📄 Java内存模型:如果有人给你撕逼Java内存模型,就把这些问题甩给他 📄 一文带你彻底理解同步和锁的本质(干货) 📄 AQS与并发包中锁的通用实现 📄 ReentrantLock介绍与使用 📄 ReentrantReadWriteLock介绍与使用 📄 ReentrantLock的Condition原理解析 📄 如何优雅的中断线程 📄 如何优雅的挂起线程 📄 图解几个好玩的并发辅助工具类 📄 图解BlockingQueue阻塞队列
📄 消息队列那么多,为什么建议深入了解下RabbitMQ? 📄 高并发异步解耦利器:RocketMQ究竟强在哪里? 📄 Kafka必知必会18问:30+图带您看透Kafka
📄 洞悉MySQL底层架构:游走在缓冲与磁盘之间 📄 SQL运行内幕:从执行原理看调优的本质 📄 洞悉Redis技术内幕:缓存,数据结构,并发,集群与算法