Java函数式编程之回调场景应用_解耦

匿名表达式(lambda)的核心,就是将代码体以参数的方式进行传递。Java里面有很多类似的思想,比如泛型就是把类型作为参数的方式传递,其中的精髓都是起着解耦和复用的作用。

而函数式编程是一种编程范式,是独立于面向过程和面向对象编程范式的。函数式编程的核心,就是把函数整体作为参数进行传递,把业务逻辑全部解耦成函数,并且通过函数的相互组合和复用形成更大的函数,最终实现业务逻辑。

而Java的核心是面向对象编程,天然就不具备函数式的特征,即无法把函数体本身进行传递,在早先的时候,一直是使用内部对象的方式来实现方法体的传递。而Java8中新增的Java函数式编程就是为了解决这个问题的。

利用Java匿名表达式实现回调

回调的概念如其名称,即“回”。“回”的本质就是A调用B的时候,B再调用结束前又调用A。因此在回调的经典案例代码一般是如下所示:

//调用方
class ClientA implement CallBack{

		public void function(){
    		B b = new B();
        //发起调用
        b.process(new ClientA());
    }
    
    public void call(){
    	//回调逻辑
    }
}

//被调用方
class B{

	void process(CallBack callBack){
  		//处理逻辑
      ...
      //发起回调
      callBack.call();
  }
}

//回调接口
interface CallBack{
	void call();
}

回调的核心,就是向被调用方注册调用的回调接口,当被调用方处理完毕业务逻辑后,发起对回调代码的处理。实际上的回调会有很多变种,比如把回调部分拆分出来成为单独的类以做更大的解耦。

例如下面的一个实际生成环境的例子,需要在页面处理的时候,给页面覆写一个页面的生成ID。正常的写法是在页面处理Main方法里先查询出来该ID,然后再SET这个取得的ID。由于生成的ID的代码比较复杂,所以把生成ID的代码封装成了一个单独的方法,使得ID生成的方法成为了一个被调用方。

  • 原先的写法
// 在Main方法里调用覆盖页面ID
String Id = this.getPageId(context);
context.setId(Id);
;


/**
     * 获取用户组织除了常用页面appUuid之外的第一个有效的页面uuid
     *
     * @param context
     * @return
     * @throws WorkbenchException
     */
    private String getPageId(SchemaContext context)  {
        // 获取组织生效的页面ID
        String adapterAppUuid = orgWorkbenchSupport
            .getUuid(context.getOrgId(), context.getAppUuid(), context.getUid());
        // 这里还隐藏了复杂的逻辑
        return adapterAppUuid;
    }


可以看到,这是一种典型的面向过程的写法,没有太大的问题。但是从这里也可以看出getPageId只提供该逻辑使用,没有需要复用的场景;其次把一些ID的设置逻辑散落到了Main方法里,不够内聚。这里就可以使用匿名表达式的方式,结合回调的思想,把代码进行优化。


  • 匿名表达式回调写法

首先是回忆一下回调的核心思想,就是【向被调用函数先注册一个回调钩子,然后被调用函数在执行到某个步骤的时候,执行钩子方法实现一个“回调”的逻辑】。用业务上的术语来描述,就是“代码再执行完毕A逻辑后,再执行B逻辑”,用这种思想可以把一个复杂的流程拆成多个流程,进而在代码层面上实现解耦。

比如上面实际生产的例子,就可以概括成下面的算法逻辑:

1.向被调用代码注册一个函数式接口
2.被调用代码编写回调逻辑
3.调用方调用被调用方法,并传递函数表达式

具体优化后的代码如下所示

// 在Main方法里调用覆盖
this.getPageId(context,uuid ->{
		context.setAppUuid(StringUtils.isBlank(uuid) ? appUuid : uuid);
});


/**
     * 获取用户组织除了常用页面appUuid之外的第一个有效的页面uuid
     *
     * @param context
     * @return
     * @throws WorkbenchException
     */
    private void getPageId(SchemaContext context, Consumer<String> buildContextAppUuid) {
        // 获取组织生效的页面ID
        String adapterAppUuid = orgWorkbenchSupport
            .getEnabledAppUuidsByRequestAppUuid(context.getOrgId(), context.getAppUuid(), context.getUid());
        // 这里还隐藏了复杂的逻辑
        //把appUuid传递覆写
        buildContextAppUuid.accept(adapterAppUuid);
    }


上面的getPageId就是起着一个被调用函数的作用,我们通过注册Consumer<String> buildContextAppUuid到getPageId函数上,告诉getPageId函数,当你执行完毕后再回调一下buildContextAppUuid方法。由于函数体比较简单,只有一句context.setId(Id),如果函数体特别复杂的情况下,我们还可以把函数体也抽象成一个接口实现,实现更彻底的解耦。


函数式接口本质即把代码体当成函数传递,当A调用B的时候,向B注册函数钩子(函数式接口),在B执行回调的时候,可以直接传递函数式表达式。即用一种更加优雅简洁的方式完成了回调函数的写法。但是值得注意的是,这种方式仅适用于函数式代码逻辑不多的场景,如果回调的代码非常多,则会使得方法A中大量暴露回调的详细逻辑,对于代码的可读性和可维护性都会降低。

比如上文这种在调用B查询ID并在A中覆盖ID这种轻量级逻辑,就可以用函数表达式的方式进行处理,就更加优雅、已读。


函数式接口介绍

函数式接口就是对于函数表达式的标准接口定义,也就是一个函数接口背后可以实现多种函数式代码。Java8里已经提供了多种默认的函数式接口的实现,大家可以直接取用。

Java函数式编程之回调场景应用_解耦_02


也可以自定义自己的函数式接口,定义起来也比较方便。定义格式如下:

@FunctionalInterface
interface GreetingService 
{
    void sayMessage(String message);
}

即用FunctionalInterface注解来标注接口,就成为了函数式接口。使用的时候就可以以函数式语法来执行:

GreetingService greetService = message-> System.out.println("Hello " + message);

更多函数式接口明细:

https://www.runoob.com/java/java8-functional-interfaces.html


更多精彩文章,可以关注公众号:ali老蒋