响应式编程是一种基于异步数据流驱动、响应式、使用声明式范式的编程模型,需要遵循一定的响应式编程开发规范,并且有具体的类库实现。响应式编程基于数据流而不是控制流进行业务逻辑的推进。
在面向对象编程语言中,响应式编程通常以观察者模式呈现。将响应式流模式和迭代器模式比较,其主要区别是,迭代器基于“拉”模式,而响应式流基于“推”模式。
在命令编程范式中,开发者掌握控制流,使用迭代器遍历“数据”,使用hasNext()函数判断数据是否遍历完成,使用next()函数访问下一个元素。在响应式编程模式中,使用观察者模式,数据由消息发布者(Publisher)发布并通知订阅者(Subscriber),而这种观察者模式本身在基于事件监听机制的响应式系统架构中被广泛使用。Java早期的Swing界面设计也是基于视图事件触发业务响应的系统工作模式。所以,从设计模式的角度讲,响应式编程并不是新鲜事物,只是响应式编程将监听的对象扩展到了更大范围:静态或者动态的Stream数据流,如下图所示。
响应式编程还借鉴了Reactor设计模式,我们通常会在高性能NIO网络通信框架中见到Reactor设计模式的身影,用来实现I/O多路复用。其基本思想是将所有要处理的I/O事件注册到一个中心I/O多路复用器上,同时主线程阻塞在多路复用器上,通过轮询或者边缘触发的方式来处理网络I/O事件。当有新的I/O事件到来或准备就绪时,多路复用器返回并将事件分发到对应的处理器中。Reactor设计模式和响应式编程类似,它们都不主动调用某个请求的API,而是通过注册对应接口,实现事件触发执行,如下图所示。
响应式编程很容易和响应式架构混为一谈。前面我们介绍了响应式宣言中的构建软件架构原则,把符合这些原则的系统称为响应式系统。如果说响应式系统与响应式编程之间具有什么关系,那就是响应式系统的架构风格是响应式的,而响应式编程是实现这个架构风格的最佳实践。从宏观角度看,响应式系统由各种不同组件相互操作、调用组成,共同响应用户请求。响应式系统涉及通信协议、I/O模型、网络传输、数据存储等多方面因素,保障系统在响应力、扩展性、容错、灵活性各方面表现出“实时”“低延迟”“轻量”“健壮”的系统特性。而响应式编程可能是这个大的系统架构下的一部分。另外,响应式系统一般是消息驱动的,而响应式编程是事件驱动的。
响应式宣言指出了两者的区别:“消息驱动”中消息数据被送往明确的目的地址,有固定导向;“事件驱动”是事件向达到某个给定状态的组件发出的信号,没有固定导向,只有被观察的数据。
响 应 式 编 程 同 时 容 易 和 函 数 式 编 程 混 淆 。函 数 式 编 程(Functional Reactive Programming,FRP)在二十年前就被ConalElliott精确地定义了。在函数式编程中,函数是第一类(firstclass)公民,函数式编程由“行为”和“事件”组成。事件是基于时间的离散序列,而行为是不可变的,是随着时间连续变化的数据。函数式编程与响应式编程相比,它更偏重于底层编码的实现细节。
从Java 8开始,Lambda表达式的引入为Java添加了函数式编程的特性,函数式编程提供了闭包的强大功能。Java中的Lambda表达式通常使用(argument)->(body)语法书写,如下所示:
下面是一些典型的Lambda表达式及其函数式接口:
在Java 8中新增加了@FunctionalInterface接口,用于指明该接口类型是根据Java语言规范定义的函数式接口。Java 8还声明了一些Lambda表达式可以使用的函数式接口。下面是匿名类和使用函数式编程方式的对比示例。
首先,使用@FunctionalInterface定义一个函数式编程接口。
然后,分别使用内部类和Lambda表达式两种方式执行业务逻辑。
可以看到,在函数式编程中,Lambda表达式允许将一个箭头函数作为参数进行传递,这样的语法表达更加简洁,而本质上由编译器推断并帮助实现转换包装为常规代码。因此,可以用更少的代码来实现相同的功能。而响应式编程的重点是基于“事件流”的异步编程范式,响应式编程通过函数编程方式简化面向对象语言语法的臃肿。响应式编程解决问题的流程是:将一个大的问题拆分为许多独立的小的步骤,而这些小的步骤都可以异步非阻塞地执行;当这些小的子任务执行完,它们会组成一个完整的工作流,并且这个工作流的输入输出都是非绑定的。实现响应式编程的关键就是“非阻塞”,执行线程不会因为竞争一个共享资源而陷入阻塞等待,空耗资源,并且最大化地利用物理资源。
响应式编程是一种声明式的编程模型,与之相对应的就是命令模式(线程控制流)的编程模型。大家对命令式编程模式比较熟悉,下面是一段常见的基于命令式编程模式的代码:
上述代码是通过变量的赋值并通过加法计算响应数据之间的对应算数关系结果。但是,这个代码有一个潜在的问题,当我们给这两个变量重新赋值时,第二次的Sum值却没有变化,与我们的期望不符,原因是缺少了执行相加的命令指令。
响应式编程的目的是通过“不可变操作符”固定这种数据,构建数据之间的关系,并正确输出结果,不会因为操作命令的遗忘和缺失导致结果的偏差,造成对应关系和结果错误,下面我们看一下如何使用响应式编程方式来固化这种模式。
下面使用Java 9的Flow API实现两个数的相加功能,按照相同思路,当传入的变量不同时,输出的Sum值也会随着变化,我们把这种对应关系构建为一个声明公式,代码实现如下:
从结果看,响应式编程模式的两次Sum值和输入的数值一致,能够达到预期效果。从这个例子中,我们已经初步接触到了响应式编程中数据源也就是事件发布者(Publisher),还有就是事件的监听回调函数集合——消费者(Subscriber)。消费者会根据next、error、complet触发函数对应关系的执行,以及数据的操作符操作,由于消费者的不可变性,可以根据原生的数据结构生成新的数据结构。相比命令式编程,响应式编程使用操作符表述了一个通用业务执行逻辑,一般可以组合达到预期效果,一般的操作符还包含map、filter、reduce等函数,这里就不再赘述了。
“普通的工程师堆砌代码,优秀的工程师优化代码,卓越的工程师简化代码”。
如何写出优雅整洁的代码,不仅是一门学问,也是软件工程的重要一环。在上一节中,我们简单介绍了响应式编程的编程范式,本节我们进一步从开发者的视角、系统的性能、满足用户需求等方面讨论不同编程范式的使用场景和特性优势。
编程范式,又称为编程模型,泛指软件编程过程中使用的编程风格,一般不同的编程范式具有不同的语法特性和差异。目前软件开发技术中常用的典型编程范式有以下几种。
因为每一个编程范式都有很长的发展历史,在编程语言支持上有不同的标准、组织和语法规范等,本节的目的是希望通过对这些编程范式的介绍,可以帮助我们更好地理解响应式编程范式。
命令式编程是非常传统的软件编程方式,命令式编程由不同的逻辑执行步骤组成,通过一步步指令的执行达到业务逻辑的推进,这种方式也称为过程式编程。命令式编程的执行过程非常符合计算机的执行步骤。C语言是命令式编程的典型代表,它更关注的是机器域底层的内存、指令计算、输入输出。在C语言中,我们经常看到大段的过程式指令、各种if/else/for等控制语句、表达式、数据变量的操作、赋值等指令,这种纯指令开发方式要求开发者对计算机的底层工作原理有非常深刻的理解,而且一个指令出现偏差往往会产生不可预知的错误。同时,命令式编程模式的运维也是难度非常高的。
面向对象编程可以说是编程领域的一个分水岭,开启了高级程序语言在软件开发上的统治阶段。面向对象编程从问题域出发,将封装、继承、多态的语言特性映射到我们的现实世界。在面向对象编程里,业务问题被抽象成类、接口模板,数据和行为被统一封装在对象内部,作为程序的基本组成单元。面向对象编程范式在提升软件重用性、灵活性和扩展性上比过程式编程更进一步,C++、Java作为面向对象编程语言的代表,屏蔽了机器底层的内存管理和机器域的管理细节。而面向对象编程虽然有较高的开发效率,但是降低了代码的运行效率,这也限制了面向对象编程在性能要求苛刻场景下的应用。
声明式编程受当前“约定优于配置”理念的影响,在软件编程开发领域中被大量应用。声明式编程范式的好处是可以通过声明的方式实现业务逻辑,不需要陷入底层具体的业务逻辑实现细节。声明式编程范式关注的焦点不是采用什么算法或者逻辑来解决问题,而是描述、声明解决的问题是什么。当你的代码匹配预先设定好规则,业务逻辑就会被自动触发执行。
很多标记性语言,如HTML、XML、XSLT,就遵循声明式编程范式,而Spring Boot基于注解方式的编程模型也是声明式编程的一个代表。
Spring框架依赖AOP和IoC编程思想降低了开发者对底层逻辑业务细节的了解程度。例如在Spring Boot中,通过@Transactional注解可以声明一个方法具备事务性的操作,当异常发生时,事务会自动回滚,保证业务逻辑的正常和数据一致性。发生在@Transactional注解背后的实现细节,开发者可以不去关心。
在函数式编程范式中,函数无疑是一等公民,函数式编程最具魅力或者最重要的特性就是不可变性。它的不可变性表现在函数式编程表达式的执行结果,只取决于传入函数的参数序列,不受数据状态变化的影响。
函数式编程中的Lambda在Java 8中被引入,可以看成是两个类型之间的关系:一个输入类型和一个输出类型。Lambda演算就是给Lambda表达式一个输入类型的值,它就可以得到一个输出类型的值。
这个计算过程也是函数式代码对映射的描述,因为函数式代码的抽象程度非常高,所以也意味着函数式代码有更好的复用性。
函数式编程和命令式编程相比,更加关注消息或者数据的传递,而不像命令式编程,关注的是指令控制流。共享数据的状态在多线程环境下会存在资源竞争的情况,往往我们需要把额外的精力投入到冲突地解决、数据状态的维护中。而函数的不可变性保证了数据在传递处理过程中不会被篡改,也不需要依赖外部的锁资源或者状态来维护并发。所以函数式编程在多核处理器中具有天然的并发性,可以最大化地利用物理资源实现并行处理功能。
目前,在JVM体系中,已经出现了越来越多函数式编程范式的语言,例如Scala、Groovy、Clojure等。在当前计算机多核、数据优先、高性能的诉求下,函数式编程具有更广阔的发展前景和未来。然而有利总会有弊,函数式编程的语法相比面向对象编程更晦涩,在大规模工程化的协调配合中,还是需要我们去权衡利弊。因为无论哪种语言范式,本质上都是工具,最终目的都是为业务服务。
来源:
https://www.toutiao.com/article/7138687931224441382/?log_from=1fe0ae5860d06_1662512946570
“IT大咖说”欢迎广大技术人员投稿,投稿邮箱:aliang@itdks.com
来都来了,走啥走,留个言呗~
IT大咖说 | 关于版权
由“IT大咖说(ID:itdakashuo)”原创的文章,转载时请注明作者、出处及微信公众号。投稿、约稿、转载请加微信:ITDKS10(备注:投稿),茉莉小姐姐会及时与您联系!
感谢您对IT大咖说的热心支持!