像 JavaScript 这种语言很早就支持闭包了,虽然 C++ 很早就有了函数指针,Java 也很早就提供了反射中的 Method 类,不过使用它们都不能算是真正的函数式编程(面向函数编程)。原因它们还不够方便和优雅。编程语言是为人类设计的语言,如果仅仅为了可实现,那任何编程思想、设计模式、架构模式都没有意义。
Java 从 Java 8 开始支持 lambda 表达式,这才算是支持函数式编程。函数式编程有什么好处呢?如果将其与依赖注入技术结合,可以很好地遵守开闭原则,实现控制反转,便于异步调用等等。在事件驱动模型中常常应用这一技术。
举个例子。原始的系统对外提供的 API(方法),其实参是此系统的输入、返回值是此系统的输出,且需要该系统的使用者先得到运行,然后该系统的使用者主动令该系统得到运行。
如果使用函数式编程,则该系统的实参可以是一个函数,且该系统的输入与输出都可以在实参提供与接收。另外,可以实现该系统与该系统的使用者之间的解耦,令该系统与该系统的使用者的运行没有主次关系。在构造算法的时候,甚至都可以无需提前知晓具体的业务,以开闭原则完成本算法的实现。
更重要的是,函数式编程提供了框架化编程的方法。框架是软件开发中最终的必然产物。任何软件在最终开发成熟之后都会框架化,框架是避免造轮子行为而必然诞生的产物。任何成熟的软件内部肯定有两个部分,一个是对很多组件都通用的公共代码,另一个是不同组件之间会不同的独特代码。当公共代码已经足够完善以至形成一个体系时,其内部会形成一套程序运行的流水线。这种情况下,独特代码的部分想调用这些公共代码是很困难的,因为自己不成体系。它能做的,只能是将自己的代码插入到公共代码这套流水线之中来运行。
而这种插入有两种方法,一种是直接简单粗暴的,直接将独特代码以源代码的形式加入到原先公共代码之中。这种方法是很多“屎山”代码的来源,会让一个源文件变又臭又长。
另一种方法就是进行重构,通过函数式编程,可以将这些公共代码封装成一个框架,其中需要运行独特代码的部分,使用接口方法的调用来代替,并将这些需要外界注入的接口对外暴露,从而使得每次加入新独特代码的时候,不需要再加入到公共代码所在的源文件中,很好地实现了开闭原则以及屏蔽了不必要的底层细节。
函数式编程的基本步骤如下:
将公共的程序运行流水线抽取出来,制成一套公共框架。
将所有的非固定代码用接口方法来代替,设置在这套公共框架中,并将接口方法对外暴露。
将非固定代码以依赖注入的方式注入到这套公共框架中。
上面的解释太抽象了,还是使用具体的代码实现意义会更大。
这里构造一个黑盒系统,该系统是一个虚假的服务器,对外提供一个回调方法。每隔一段时间,该服务器就会调用这个回调方法来通知外界自己接收到了信息,并传递这个信息。
在给出具体的代码之前,需要介绍以下这些概念:钩子(hook)、处理器(handler)。钩子是系统对外提供的一个回调方法,该系统的使用方负责提供该方法的实现。处理器是系统内部这个回调方法的调用方。更多的信息,可见笔者的另一篇博客:
代理、委托、钩子与打桩:
https://blog.csdn.net/wangpaiblog/article/details/115436520
为了使用代码更简洁,这里使用了 Lombok,不过本文不打算详细介绍 Lombok。
这里使用 事件驱动模型
来描述这一情景。当服务器收到信息时,它就会为之生成一个事件(event),此事件中包含了 API 调用方与 API 内部之间交互的必要信息,然后该服务器会调用外界使用方提供的那个回调方法,并将此事件作为实参传递。
在 JavaScript 中实现上述情景相当简单,不过这在 Java 中略显麻烦。首先,需要定义一个事件类。这个类的字段中储存了需要传递该使用方的必要信息。
package org.wangpai.demo.fp.blackbox.event;
import java.util.Map;
import lombok.Getter;
@Getter
public class Event {
private Map<Object, Object> data; // value 为具体的数据,key 为为 value 而起的名字
public Event(Map<Object, Object> data) {
super();
this.data = data;
}
}
然后,需要构造一个处理器。构造处理器的目的不仅是为了调用回调方法,更重要的是为了储存这个回调方法。这里因为 Java 是完全的面向对象语言,数据的最小粒度是对象,因此外界传入的回调方法需要通过使用一个对象来保存。
用对象来保存方法?这看来是一个新颖的说法,但实际上,现在的高级编程语言基本上都提供了这样的功能。世界上的任何活动都可以归结为数据以及对数据的操作,这实际上就是面向对象中的字段与方法。因此,如果能使用对象,那就基本上可以干任何事情。
注意,为了能使用 Lambda 表达式这种语法糖,处理器需要是一个函数式接口。
package org.wangpai.demo.fp.blackbox.handler;
import org.wangpai.demo.fp.blackbox.event.Event;
@FunctionalInterface
public interface Handler {
void handle(Event event);
}
现在可以开始构造一个虚假的服务器了。该服务器要作的事情很简单:每隔一段时间调用一次回调方法,通知使用方接收到了信息。
package org.wangpai.demo.fp.blackbox;
import java.util.HashMap;
import lombok.Setter;
import lombok.SneakyThrows;
import org.wangpai.demo.fp.blackbox.event.Event;
import org.wangpai.demo.fp.blackbox.handler.Handler;
@Setter
public class MockServer {
private Handler onReceiveHandler;
@SneakyThrows
public void start() {
System.out.println("---方法 start 开始调用---");
for (int index = 1; index <= 10; ++index) {
Thread.sleep(1000); // 每次休眠 1 秒
if (onReceiveHandler == null) {
continue; // 如果使用者没有提供回调,什么也不做
}
var msgData = new HashMap<String, Object>(1);
msgData.put("text", "接收到第 " + index + " 条信息");
onReceiveHandler.handle(new Event(msgData));
}
System.out.println("***方法 start 结束调用***");
}
}
写完服务器的代码就可以进行测试了。测试很简单,模拟服务器的使用方,对服务器接收到的信息进行控制台输出。
package org.wangpai.demo.fp.blackbox;
import java.util.concurrent.Executors;
public class MockServerTest {
public static void main(String[] args) {
var server = new MockServer();
server.setOnReceiveHandler(event -> System.out.println("来自服务器的反馈:" + event.getData().get("text")));
Executors.newCachedThreadPool().execute(() -> server.start());
System.out.println("***方法 main 结束调用***");
}
}
运行结果如下:
可以看出,对于服务器的使用方,仅仅需要提供一个回调即可实现接收与服务器主动传入的信息。
上述的代码虽然已经实现了 事件驱动
功能,不过也有此不足之处。如果服务器类存在很多个回调,那使用方就需要实现很多个回调方法。对使用方来说,这很容易造成遗漏。虽然服务器可以为每个回调提供一种默认实现,不过有些场景下要求使用方一定要提供实现。
实现这个需求最好的方法是将服务器所需的所有回调放入一种 抽象类
中。很多编程语言都提供了抽象类这种功能。对于抽象类来说,它强制要求非抽象子类实现它的所有抽象方法。而在 Java 中,更好的方式是使用接口。在这里,这个接口不妨叫就做 Hooks。
package org.wangpai.demo.fp.blackbox.hook;
import org.wangpai.demo.fp.blackbox.event.Event;
public interface Hooks {
void onReceiveData(Event event);
void onDestroy(Event event);
}
现在,需要一个类来将这整个接口中的这些个回调方法与各个处理器相应对应,这个类不妨叫就做 Handlers 类。
package org.wangpai.demo.fp.blackbox.handler;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.Setter;
import lombok.experimental.Accessors;
import org.wangpai.demo.fp.blackbox.hook.Hooks;
@Setter(AccessLevel.PROTECTED)
@Getter(AccessLevel.PROTECTED)
@Accessors(chain = true)
public abstract class Handlers {
private Handler onReceiveHandler;
private Handler onDestroyHandler;
public Handlers setHooks(Hooks hooks) {
this.onReceiveHandler = hooks::onReceive;
this.onDestroyHandler = hooks::onDestroy;
return this;
}
}
上面的代码看起来很像 C++
代码,但很遗憾,它就是 Java 代码。上面使用的双冒号::
语法叫做方法引用(method reference)。出于本文的重点,这里不做详解。
实现了上面的 Handlers 类之后,服务器类可以选择通过继承或者组合来并入该类。简单起见,这里选择继承。不过这里有个问题,服务器需要使用 getXXXHandler().handle(event)
来调用这个回调方法。虽然可以这样做,但这太底层了,最好是来构造一个类来过渡一下。有人认为这没有必要,但每个人在使用其他人的代码的时候,都想着尽量可以不需要看对方的源码。起一个好的名称,提供逻辑更通顺简单的接口能大大减少使用者的工作量。这里选择再构造一个类进行过渡。
package org.wangpai.demo.fp.blackbox;
import org.wangpai.demo.fp.blackbox.event.Event;
import org.wangpai.demo.fp.blackbox.handler.Handler;
import org.wangpai.demo.fp.blackbox.handler.Handlers;
public abstract class OnServerAction extends Handlers {
public final void setOnReceive(Handler handler) {
this.setOnReceiveHandler(handler);
}
public final void onReceive(Event event) {
this.handle(this.getOnReceiveHandler(), event);
}
public final void setOnDestroy(Handler handler) {
this.setOnDestroyHandler(handler);
}
public final void onDestroy(Event event) {
this.handle(this.getOnDestroyHandler(), event);
}
private void handle(Handler handler, Event event) {
if (handler != null) {
handler.handle(event);
}
}
}
另外,上面 Event 的 Map 中的 key 值似乎太随意了,而且也不便于管理。最好是能够约定 Event 携带的数据到底可以是哪些类型。
package org.wangpai.demo.fp.blackbox.event;
public enum DataType {
TEXT,
BINARY
}
相应的类 Event 修改如下:
package org.wangpai.demo.fp.blackbox.event;
import java.util.HashMap;
import java.util.Map;
public class Event {
private Map<DataType, Object> data;
private Event() {
super();
this.data = new HashMap<>(2);
}
public Object getData(DataType dataType) {
return data.get(dataType);
}
public Event setData(DataType dataType, Object data) {
this.data.put(dataType, data);
return this;
}
public static Event getInstance() {
return new Event();
}
}
但是,Event 内部是用 Map 来存储数据的,如果事先知道传输的数据类型(Map 中的 key 值)呢?可以选择遍历 Map,看看都储存了哪些数据,但是这样做的耦合度太高了。
一个解决办法是,令 Event 的发送方与接收方事先约定 Event 会携带哪些字段。
另一个解决办法是,使用一个所谓的 Head-Content
协议。这种传输方式要求使用额外的空间来记录所传输的数据的一些重要信息。为此,可以将上述 EventType 修改为如下:
package org.wangpai.demo.fp.blackbox.event;
public enum DataType {
HEAD,
TEXT,
BINARY
}
然后让服务器在传输时,将数据与数据的类型一起传输。
对于本示例中的简单情形,看不出这样做的明显好处,这看起来与直接遍历 Map 没有区别,甚至可以 List 或者直接用 Object 来代替 Map。不过,如果传输的数据个数有很多且有冗余,或者需要以职责链模式来依次处理 Event 中的数据,这样做就能保证分层处理数据时的井然有序。
package org.wangpai.demo.fp.blackbox;
import lombok.SneakyThrows;
import org.wangpai.demo.fp.blackbox.event.DataType;
import org.wangpai.demo.fp.blackbox.event.Event;
public class MockServer extends OnServerAction {
@SneakyThrows
public void start() {
System.out.println("---方法 start 开始调用---");
for (int index = 1; index <= 10; ++index) {
Thread.sleep(1000); // 每次休眠 1 秒
if (this.getOnReceiveHandler() == null) {
continue; // 如果使用者没有提供回调,什么也不做
}
var event = Event.getInstance();
var dataType = DataType.TEXT;
event.setData(DataType.HEAD, dataType);
event.setData(dataType, "接收到第 " + index + " 条信息");
this.onReceive(event);
}
if (this.getOnDestroyHandler() != null) {
this.onDestroy(null);
}
System.out.println("***方法 start 结束调用***");
}
}
对于使用方,只需要构造一个 Hooks 对象。这需要实现其中的所有方法,这就能防止使用方忘记实现某个回调方法。
package org.wangpai.demo.fp.blackbox;
import java.util.concurrent.Executors;
import org.wangpai.demo.fp.blackbox.event.DataType;
import org.wangpai.demo.fp.blackbox.event.Event;
import org.wangpai.demo.fp.blackbox.hook.Hooks;
public class MockServerTest {
public static void main(String[] args) {
final var executor = Executors.newCachedThreadPool();
var hooks = new Hooks() {
@Override
public void onReceive(Event event) {
var dataType = (DataType) event.getData(DataType.HEAD);
System.out.println("来自服务器的反馈:" + event.getData(dataType));
}
@Override
public void onDestroy(Event event) {
System.out.println("服务器停止信息接收");
executor.shutdown();
}
};
var server = new MockServer();
server.setHooks(hooks);
executor.execute(() -> server.start());
System.out.println("***方法 main 结束调用***");
}
}
运行结果如下:
已上传至 GitHub 中,可免费下载:https://github.com/wangpaiblog/20220201-functional_programming