使用函数式编程可以减少代码重复,使代码更易于理解。
JAVA编程语言以其面向对象的特性而闻名,但也因其冗长和繁琐的异常处理机制而而广受批评。当Java语言在1.8版本引入函数式编程能力时,人们并没有马上理解到这如何给程序员提供帮助。
本文给大家讲解一个示例,以说明函数式编程如何提高代码的重用性和可读性。
为了实现功能,第三方编写的通信客户端使用了依赖注入和注释。然而,该客户端存在一些问题,例如抛出已检查异常、缺乏日志记录和重试能力。因此,我们需要对该客户端的功能进行封装,以添加重试功能、日志记录和良好的异常处理。
如果没有函数式编程,那么程序需要创建一个外观,将每个客户端功能都包装在委托中,并添加日志记录、异常处理和通信重试的逻辑。客户端共有50个需要封装的函数,这将导致几乎完全相同的50个副本的代码,仅有调用的客户端函数和传递的数据类型有所不同。这样会带来大量的重复代码问题。
为了解决这个问题,要使用函数式编程的方式对该客户端功能进行封装,以实现重试能力、日志记录和良好的异常处理。
解决方案是添加1或2个专门用于处理异常、执行日志记录和实现重试循环的方法。但是,它们需要调用通信客户端中的特定函数。
使用函数式编程,方法可以将函数作为其参数接收。该方法不需要在设计时知道哪个函数。Java(因为这与其他编程语言不同)所要求的是函数具有预期的签名。
在我们的情况下,我们需要2种变体:BiFunction签名和BiConsumer签名。区别在于BiFunction返回一个值,而BiConsumer则不返回。
public final ActionResult<List<Order>>
downloadOrders() {
return get(
".downloadOrders()",
(
client,
apiKey
) -> client.downloadOrders(apiKey.toString())
);
}
上面的示例没有显示错误处理、日志记录和重试循环。这是由get(String, BiFunction)方法执行的。因此,我们不需要50个重复的错误处理、日志记录和重试循环,而是有50个易于理解的委托。
get(String, BiFunction)方法回调传入的BiFunction,我们在上面看到了这一点:
(
client,
apiKey
) -> client.downloadOrders(apiKey.toString())
我们可能会从JavaScript中认识到这一点。语法是一个BiFunction的lambda表示法:它接受2个参数,可能做一些事情,并返回一些东西。它是一个回调函数,根据需要即时创建,并且因为它没有名称,所以它保持匿名(并由编译器分配标识)。它的参数client和apiKey在方法get(String, BiFunction)内生成,并在运行时传回回调函数中。它看起来像这样:
private final <T>
ActionResult<T>
get(
final String caller,
final BiFunction<
Client,
ApiKey,
T
> callback
) {
final Client client = Client.newInstance(caller);
final ApiKey key = this.getKey();
final MutableList<Exception> errors = Lists.mutable.empty();
boolean mustRetry = true;
for (
int retryCount = 0;
mustRetry && retryCount < MAX_RETRIES;
retryCount += 1
) {
try {
return new ActionResult<T>(
callback.Apply(
client,
apiKey
)
).with(errors);
} catch (Exception ex) {
mustRetry = mustRetry(ex);
if (mustRetry) {
try {
TimeUnit.SECONDS.wAIt(1 << (retryCount + 1));
} catch (InterruptedException ie) {
errors.add(ie);
}
} else {
errors.add(ex);
}
}
}
return ActionResult.<T>empty()
.with(errors);
}
在那段代码中,BiFunction通过声明进行回调:
callback.apply(
client,
apiKey
)
请注意,get(String, BiFunction)不知道回调返回的数据类型。在像Java这样的强类型和显式类型编程语言中,通常不可能。直到泛型引入Java编程语言之前,这是不可能的。这就是为什么代码中存在:它是回调返回的数据类型的占位符。
请注意,调用站点也没有指定返回值的数据类型。相反,它由客户端函数的返回值和委托上指定的返回数据类型隐含。如果它们不匹配,编译器将发出警告,保持数据类型良好且检查过。
坏的抽象是面向对象编程中许多问题的根源。我们希望将目前的两个异常处理和重试循环包装器减少到一个包装器,但是由于一个接受BiFunction回调,另一个接受BiConsumer回调,还没有找到合适的方法。因为从概念上讲,它们执行不同的操作:发送和接收。使用相同的包装器可能会破坏这种概念上的区别。总之,对于现在来说值得高兴的是避免了重复使用50个包装器的情况。
当然,有些优秀的编译器可能会检测到代码重复,并采取一些技巧将它们减少为具有不同回调的单个包装器。然而,这并非我们当前关注的重点。我们的目标是消除重复代码并提高代码的可读性。
从理论上讲,使用这个包装器和函数委托可能会导致程序变慢。然而,根据经验,由于与远程通信伙伴进行通信时遇到的网络延迟,潜在的几毫秒延迟相对微不足道。尽管如此,如果在您的环境中这导致了明显的性能下降,请测量吞吐量并进行相应的调整。
静态代码分析工具若能提供有关消除重复代码的建议,将对代码优化非常有益。这样的工具可以帮助开发人员识别和消除重复的代码段,从而提高代码的可读性、可维护性和性能。