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