Java 函数式编程思考 —— 授人以渔

2023-09-17 16:05:50

引言

最近在使用函数式编程时,突然有了一点心得体会,简单说,用好了函数式编程,可以极大的实现方法调用的解耦,业务逻辑高度内聚,同时减少不必要的分支语句(if-else)。

一、函数式编程就是Lambda表达式吗?

Java语言早在 JDK8 就提供了函数式编程的基础。

你可能会问,函数编程不就是lambda表达式吗?

的确,大多数开发可能还停留在 lambda 表达式的使用层面,但请注意,我从标题、文章开篇都在强调“函数式编程”,很明显,我有意区别函数式编程和lambda表达式两者的概念。

Java 8 引入的函数式编程到底是什么?最近我在开发过程中遇到了一个场景,才让我解开了这个困扰我的问题。

二、一个小场景——多路调用

我遇到的场景其实并不复杂,或者说我们每天都在写如此场景,我甚至并不知道这么简单的场景有没有相应的专有名词来表示,就暂将其称为“多路调用场景”。让我们简单模拟一下。

场景描述
A Service 和 B Service 都依赖 CService。
A 和 B都用到了 C的一个方法 doProcess(),但 doProcess() 在处理 A 和 B的请求时,又需要拿到 A 或 B 的业务数据。
该如何实现 doProcess() 方法?

@AllArgsConstructor
class AService {
    private CService cService;

    public void processData() {
        List<Object> all = this.getAll();
        cService.doProcess(all);
    }

    private List<Object> getAll() {
        return Collections.emptyList();
    }
}

@AllArgsConstructor
class BService {
    private CService cService;

    public void processData() {
        List<Object> data = this.queryBDatas();
        cService.doProcess(data);
    }

    private List<Object> queryBDatas() {
        return new ArrayList<>();
    }
}

class CService {
    public void doProcess(List<Object> busiData) {
        // 执行C自己的处理逻辑...
        busiData.stream().forEach(d -> {
            System.out.println(d);
        });
    }
}
/**
 * 测试代码
 */
public class Test {
    public static void main(String[] args) {
        // 实例化服务对象
        CService cService = new CService();
        AService aService = new AService(cService);
        BService bService = new BService(cService);

        // A -> C 处理请求
        aService.processData();

        // B -> C 处理请求
        bService.processData();
    }
}

如上代码所示 A、B 的processData 方法都调用C的doProcess 方法,他们都将 doProcess 所需的数据通过参数传递过去。这种实现方式虽然可以成功的适配不同的调用者,但是数据的生成是在调用 doProcess 前,一旦doProcess执行了一些校验逻辑而无法用到这些已经准备好的数据,就可能白白浪费查询资源。

另一种实现是通过循环依赖,将A或B的实例反过来也注入到 C 服务中,在 doProcess 中,需要用到A 或 B 的数据时才去查询。这种实现方式虽然可以实现懒加载,但又引入了另一个问题,就是高耦合性,而且依然需要通过 if-else 判断具体是要执行 A.getAll 还是需要执行 B.queryList,代码冗杂不说,扩展性也很糟糕。

当然,上述代码只是个模型,实际业务可能比这还要复杂。那到底有没有一种,既可以实现懒加载,又高度内聚,不需要循环依赖的实现方式呢

三、授人以鱼不如授人以渔

请原谅我起了一个这么哗众取宠的小节标题,我后面会解释。

3.1 传统思路的弊端

传统的实现思路,将数据提前准备好传递过去,或使用循环依赖,增加判断条件,执行不同的业务逻辑。

似乎这类实现已被大家习以为常,但就像前面描述的,数据传递可能会造成性能、资源等浪费;传统的延迟处理又需要搭配循环依赖,从而造成严重的依赖混乱问题,明明公共服务被业务服务依赖,公共服务却反过来还要依赖业务服务,扩展性极低。

函数式编程可以很好的解决这个问题!

3.2 授人以渔

Java 8 引入的函数式编程,允许开发者将函数像参数一样传递

直到最近,我才终于理解了这个定义。函数,实际上就是一个具体的处理逻辑,一个解决方案,一个“捕鱼的方法”、一个可开箱即用的“锦囊妙计”。

我们将函数传递到另一个方法中,那么在这个方法中,就可以直接去执行这个函数。

再回到上面的场景中,A和 B调用 C 的 doProcess() 方法,如果使用函数式编程,该如何实现?

首先定义一个业务数据查询函数

// 业务数据查询函数
@FunctionalInterface
public interface BusiDataQueryFunc {
    List<Object> queryList();
}

然后将其作为参数声明到 doProcess 参数中,令A或B调用时传递一个具体的实现逻辑,如下所示:

@AllArgsConstructor
class AService {
    private CService cService;

    public void processData() {
        cService.doProcess(() -> this.getAll());
    }

    private List<Object> getAll() {
        // A 业务数据查询逻辑
        return Collections.emptyList();
    }
}

@AllArgsConstructor
class BService {
    private CService cService;

    public void processData() {
        // 传入数据查询函数
        cService.doProcess(() -> this.queryBDatas());
    }

    private List<Object> queryBDatas() {
        // B 业务数据查询逻辑
        return new ArrayList<>();
    }
}

class CService {
    public void doProcess(BusiDataQueryFunc busiDataQueryFunc) {
        // 查询调用者所需数据
        List<Object> busiData = busiDataQueryFunc.queryList();
        // 执行C自己的处理逻辑...
        busiData.stream().forEach(d -> {
            System.out.println(d);
        });
    }
}

如此,在doProcess中,就不需要使用任何的 if 判断,同时也实现了懒加载获取到业务的数据。相比传统的将数据传递,或通过特定参数加if-else按条件查询,耦合度更低,这是集性能强、扩展性强、耦合度低等优点于一身的优秀实现方式

如果把数据传递到方法中,比作授人以鱼,那么函数传递,就是授人以渔。将“捕鱼的方法”告诉被调用者,这就是为什么我将函数式编程称为——授人以渔的开发思想。

四、总结

在多路调用的场景中,通常会需要在被调用方法中使用到调用者的一些数据,传统的编程方式是直接将数据作为参数传递过去,或者通过一些业务标识用if-else的方式来判断该调用哪个业务方法。

直接传递数据的方式,提前将数据准备好,会有性能问题,可能在被调用方法的校验逻辑执行中断,用不到数据,浪费系统资源;而通过普通的if-else 分支,又需要将调用者注入到被依赖方,虽然实现了懒加载,但本身形成了循环依赖,造成了高耦合,存在潜在的开发成本。

所以,Java 8 提供的函数式编程,将获取数据的方式通过函数传递给被调用者,授人以渔,即满足懒加载,又解耦了依赖关系。这在依赖关系复杂的系统中是一个非常有用的设计思想。
在这里插入图片描述

更多推荐

QT 调用USB免驱摄像头

文章目录前言一、界面布局二、QImageEncoderSettings类三、图像的显示总结前言本篇文章来讲解一下如何使用QT调用摄像头,这里我使用的是USB免驱动摄像头,使用不需要按照驱动QT就可以调用到摄像头。一、界面布局这里使用QT设计师进行界面的布局:二、QImageEncoderSettings类QImageE

【Linux网络编程】Socket-TCP实例

该代码利用socket套接字建立Tcp连接,包含服务器和客户端。当服务器和客户端启动时需要把端口号或ip地址以命令行参数的形式传入。服务器启动如果接受到客户端发来的请求连接,accept函数会返回一个打开的socket文件描述符,区别于监听连接的listensock,它用来为客户端提供服务的。因为有线程池的存在,可以立

Linux设备驱动模型之platform设备

Linux设备驱动模型之platform设备上一章节介绍了Linux字符设备驱动,它是比较基础的,让大家理解Linux内核的设备驱动是如何注册、使用的。但在工作中,个人认为完全手写一个字符设备驱动的机会比较少,更多的都是基于前人的代码修修补补过三年。在内核驱动中,你会看到比较多的platform相关的字样,他们具体是什

涨知识,关于代码签名证书10大常见问题解答

在当今互联网+时代,各种软件程序充斥着这个网络世界,大大小小的软件层出不穷,如何让用户信任软件并下载软件,是众多软件开发公司需要解决的问题,由此代码签名证书应运而生,提供了软件程序的身份认证、完整性和可信任性的解决方案。那么什么是代码签名证书?代码签名证书的原理是什么?锐成小编收集整理了关于代码签名证书10大常见问题解

PostgreSQL的主从复制方式

主从复制方式PostgreSQL支持多种主从复制(Master-SlaveReplication)方式,用于创建可靠的数据备份和故障容错解决方案。以下是几种常见的主从复制方式:同步复制(SynchronousReplication):在同步复制中,主节点将事务发送到一个或多个从节点,并等待从节点确认写操作已成功应用。只

AIGC驱动产品开发创新,改变你所知的一切!

你是否想过,3000年后的饮料是什么味道?9月12日,可口可乐全球创意平台“乐创无界”再度推出全新限定产品——首款联合人工智能(AI)打造的无糖可口可乐“未来3000年”。从口味研发到包装设计都体现了AI的深度参与打造。Y3000与AI共创这一举措,也呼应了平台以潮流创新的产品与沉浸式体验链接年轻消费者的理念,赋予可口

多因素身份验证MFA功能

随着信息技术的不断进步,网络威胁也随之不断升级和演化。为了保护敏感数据和网络资源,企业和组织需要采取更多的安全措施强化信息安全。多因素身份验证(MFA)已经成为了现代安全战略的核心组成部分之一。在这篇文章中,我们将深入探讨ADSelfServicePlus产品内置的多因素身份验证(MFA)功能,以及如何利用ADSelf

Jmeter对图片验证码的处理

​jmeter对图片验证码的处理在web端的登录接口经常会有图片验证码的输入,而且每次登录时图片验证码都是随机的;当通过jmeter做接口登录的时候要对图片验证码进行识别出图片中的字段,然后再登录接口中使用;通过jmeter对图片验证码的识别方法1、通过ocrserver工具识别图片验证码;如下图:解压后双击OcrSe

自动直播软件开发方案:打造智能化、高效化的直播体验

一、引言随着社交媒体和互联网的快速发展,直播已经成为了人们互动和传播的重要方式。然而,传统直播存在着人力成本高、内容质量不稳定等问题,因此,开发一款自动直软件成为了解决这些问题的关键。二、市场需求分析1.直播行业的快速增长:直播行业在近年来呈现出爆发式增长的趋势,用户对高质量、多样化的直播内容的需求日益增长。2.直播内

服务器的维护是如何操作的

服务器的维护是如何操作的服务器可以说是不可或缺的资源,因为现在网络技术发达,我们的生活也都离不开网络的存在,我们想要获取的业务、资料等大多是通过网络进行,所以想要顺应潮流并获得发展,肯定需要服务器来将企业的相关信息与产品等发布到网络中,供客户选择。那应该如何维护好服务器呢?硬件维护1、增加内存和硬盘容量的工作。增加内存

转行车载做开发,首先得掌握好Android Framework~

前言在当今社会,科技的快速发展推动了各行各业的变革。移动设备作为人们生活、工作、学习的重要工具,其市场份额逐年攀升。Android作为全球最受欢迎的移动操作系统之一,为开发者提供了广阔的发展空间。但是现在Android发展比较尴尬,之前想吃它红利的人群太多,加快了行业内卷的部分,致使现在Android人才市场饱,甚至可

热文推荐