类型流(TypeFlow)——世俗化的函数式编程和改进的过程式设计
软件手艺人,ThoughtWorks咨询师
众所周知,客户需求的自然形态是面向过程(或者叫结构化编程)的。 你在任何项目上,跟业务专家聊需求,你得到的都是先做什么、后做什么;流程、子流程。
至于面向对象,是一种设计方法,并不是所谓最接近现实世界的设计思想,反而是设计师硬凹过来的。只不过现在的程序员上学就学的面向对象,受面向对象训练良久,已经忘了面向过程了。不信你可以把你的类图拿给不懂技术的业务需求方,解释给他们听,问问业务专家脑子里的现实世界是不是这样的。
但面向过程毕竟被淘汰了,而且淘汰是有理由的:
当一个团队多人共同开发一个应用的时候,由于过程之间存在依赖,而每个过程都可以操作任何资源,并且过程和资源的关系不是显式的,这就使不同开发者之间产生互相干扰,而且是隐式的。 因此,随着业务和系统复杂度的提高,和开发团队规模的增加,面向过程只能被淘汰。
面向对象强调行为和数据的封装。某种角度来说,相当于说我的资源只有我能碰,你不许碰,你的资源只有你能动,我也不碰。 我们之间只能通过公开的接口(或消息)来交互。 从类比的角度看,一个微服务有点像面向对象的宏观体现。 其内部数据(聚合)只允许本服务自己操作,别的服务只能通过这个服务的API来访问。 这样在一定程度上降低了开发者的互相干扰。
但是,一个微服务比较大且复杂的时候,微服务内的开发团队往往也不止一人,这些内部的开发者之间仍然存在着干扰。 和原本的面向过程没有本质区别。
另一个问题就是面向对象本身缺乏统一认识,太多争议。 贫血模型和充血模型之争,继承和组合之争,静态方法的问题等等,还有很多case by case的争论。一个设计经常被一些人说不OO,同时被另一些人说不实用。Java的面向对象和和AKKA或者说Actor模型所体现的面向对象又不一样。包括和传说的面向对象鼻祖smalltalk(我没有经验)又有区别。
最近这一两年,我们越来越多指导客户团队做DDD落地的咨询项目,当面对客户大规模的厂商团队(而且常常是来自多个厂商)的时候,大量的junior的厂商开发人员是不理解面向对象的,而不同厂商的高级开发人员互相之间以及和我们之间都是没有统一认识的,这一点对大规模应用的维护带来了非常大的麻烦。
而很多人寄予厚望的函数式编程,虽然有很明显的优点,而且内部争议不太多,但是始终无法大规模使用。 经过多年的努力推广,我已经深深认识到:如果开发人员必须要理解了Monad、MonadTransformer之类的概念才能用上函数式编程,那么函数式编程注定只能在小圈子里流行。
因此,在去年(2019年11月)我提出了一种新的设计方法论,叫做类型流(TypeFlow)。这种方法论可以算是一种世俗化的函数式编程和改进型的过程式设计。 它的思想可以用下面这张图表达。
类型流有以下特点:
- 类型流采用了函数式编程的核心概念之一,纯函数来体现业务逻辑。 对副作用部分显式的体现出来,而不是包装在IO Monad里。
- 类型流以实现端口适配器架构为设计目标,达到业务逻辑和技术基础设施的分离。
- 同时,类型流提供可视化建模的构造块,使之成为DDD落地使最细粒度的一块拼图,在大量厂商人员的项目上可以作为详细设计的一部分。
类型流设计建模的构造块如下:
以一个TODO应用为例,创建代办事项设计图如下:
这个例子较为简单,但已经可以体现出类型流方法论的主要规则:
- 从可视化模型上就可以看出:共存在4个待实现的函数,其中两个纯函数,参数校验和返回结果包装;一个副作用函数,保存代办事项;还有一个输入端口,即把这个几个函数编排起来完成业务的程序入口。
- 每个函数有明确的输入输出类型
- 函数之间通过匹配的输入输出类型连接起来。
- 输入输出类型使用业务人员能够理解的业务概念,从而符合DDD的要求。
- 可视化
这些规则带来以下特点:
- 副作用剥离,有副作用的没有业务逻辑,有业务逻辑的没有副作用。
- 每个函数都不知道自己的入参从哪里来,自己的输出被谁使用,这几个函数的开发者也不需要知道。只要按照设计实现了函数,就能够拼接起来。
- 类型的约束在整个流上层层加强,比如参数校验合法的创建代办事项请求和原始的创建代办事项请求所包含的数据可能是完全一样的,但是他们的数据类型是不同的,在强类型的编程语言实现上如果误传参数是编译不过的。使程序很难写错。
这些特点带来以下优势:
-
由于副作用剥离,业务逻辑内聚在纯函数、输入输出类型和流程本身;副作用函数是与技术基础设施的交互,不包含业务逻辑,因此可以轻松的替换掉,而不影响任何业务逻辑,达成了端口适配器架构的目标。在我的配套工具原型上,可以轻松的从本地文本文件改成Serverless+对象存储而不修改任何业务逻辑。 参见我以前发布的视频
-
纯函数和副作用函数都变得非常好测。
-
由于函数之间的低耦合,开发任务可以分配给任何开发人员并行开发。
-
可视化的模型将系统的实现细节完整保留,为知识的保留和其他开发人员接手代码提供了抓手。
-
由于类型流图提供了非常多的细节信息,因此可以开发出强大的配套工具支持:
-
- 由于每个函数都定义了明确的输入输出,因此可以生成准确的函数骨架,程序员只需要填空
- 由于区分了副作用函数和纯函数,可以只给副作用函数生成相应的数据库连接或外部系统stub,加强高级程序员对代码实现的控制。
- 由于类型流图已经提供了足够的信息,入口函数的调用链是可以自动生成的。
- 由于每个函数都体现为输入和输出类型,因此如果某个输出类型没有得到有效处理(流不完整),是可以自动检测出来的。
- 可以Diff,看不同版本变了什么
类型流最核心的思想是显式的副作用剥离,通过这一点得到函数式编程的好处,而不引入函数式编程的难点。再放一张图解释一下副作用剥离的常见模式:
把上图的一个大函数剥离后,得到了纯函数修改业务数据状态,这个函数不需要知道入参是从哪个数据库表里查来的,也不需要知道它自己的输出要被写到哪里去。这个函数变得极其容易单元测试。图上还有两个未连接的输出,工具应该很容易检测出来。
这个图和流程的区别在于:除了样子有点像,他们完全就不是一种东西。流程图里的每一步都是一个过程,这个过程可以做任何事情,操作任何资源,从流程图上是完全看不出它操作了什么资源的。因此流程图完全继承了过程式编程的所有缺点。
类型流的团队组织:
这么长的文章看到这里,大家想必也应该能看出来,类型流是针对大团队、细分工组织设计的方法论。 高级程序员团队画类型流图建模,利用工具生成代码骨架,初级程序员填空。高级程序员团队仍然是一个敏捷团队,而初级程序员只有基本的编程要求。甚至单元测试能力也不要求。刚才我虽然说纯函数很容易单元测试,但其实也很容易开发一个配套的测试工具,可以读取文本的输入参数列表和期望返回值列表,可以让专职的测试人员或者BA去填。 所以是对高级程序员提供了要求,对初级程序员极大降低了要求的方法论。
类型流的适用场景:
- 复杂业务+大规模团队。类型流模型直接对应到代码,能够帮助业务系统的长期维护。 借用@李磊同学的话来说:厂商项目交付是客户自己工作的开始。
- 银行核心系统上云。银行核心系统很多运行在主机上的RPG和COBOL是过程式的。如果原样搬,只换种编程语言,则保留了过程式的全部缺点,而运行环境换成了不如主机可靠的分布式环境,后果难料。 如果换面向对象重新设计,则等于完全重新设计,难度大、风险大。类型流的副作用剥离加可视化建模是性价比很高的改进方法。
- Serverless应用。 Serverless(Function as a Service)具有非常多的好处,但是现在业界的使用场景仍然非常局限。我认为主要是业界还没有找到一个完整业务应用应该如何拆解成函数从而在Serverless基础设施上跑起来。而当前各厂商提供的一些流程编排工具和框架都是非常过程式的。都是不适合多人协作大项目的。类型流具有很大的潜力成为拆解Serverless函数的方法论。
从2019年11宣布以来,由于种种原因,工具方面进展缓慢,但方法论本身在好几个客户那里,作为DDD落地方法的详细设计一级的方法得到了应用,取得不错的效果,也越来越完善。在此介绍给大家参考,欢迎意见和建议。