函数式编程实战改进完整代码
像 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
然后,需要构造一个处理器。构造处理器的目的不仅是为了调用回调方法,更重要的是为了储存这个回调方法。这里因为 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(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 Mapdata; 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 结束调用***"); } }
运行结果如下:
完整代码已上传至 GitCode 中,可免费下载:https://gitcode.net/wangpaiblog/20220201-functional_programming
欢迎分享,转载请注明来源:内存溢出
评论列表(0条)