Go语言推崇的CSP编程模型和设计思想,并没有引起很多Go开发者包括Go标准库作者的重视。标准库的很多设计保留了很浓的OOP的味道。本篇Blog想比较下从设计的角度看,CSP和OOP到底有什么区别。
下面,我们来看一个例子,如果我们有一个项目,需要做一个TCP连接中继器(请原谅我的用词)。我们先按照OOP来设计下:
=====
好,我们再按CSP的思路来设计下,是这么一个过程:
=====
OOP部分写的比较简略,但是设计思路还是能看出来的,OOP的设计 核心的围绕点是系统中的对象的种类、职责以及相互的关系;OOP在低并发的时代诞生,对于系统中动力分配是不怎么重视的。在遇到具有共性的点的时候,OOP多是用接口的形式表达,多个不同类实现同一个接口。
CSP的设计 核心的围绕点,是系统中的动力源,也就是系统中动力的来源。动力源在Go语言里就是goroutine;由于goroutine往往是通过闭包函数创建出来,所以闭包函数捕获的upvalue等,也就成了父goroutine和子goroutine之间的一种隐藏的协议。更重要的,每个协程,本质上就是在调度发生时,自动把 上下文 保存起来的回调函数。这大大简化了状态的维护工作。在遇到具有共性的点的时候,CSP是多用装饰器和桥接器,把系统中的共性用函数的参数表达出来。
这两种设计中,接口和函数其实可以相互转换,一个接口只有一个方法的等价于一个函数;而几个函数构成了固定的组合,在Go里面等价于实现了某个接口。所以,这种对共性抽象的方法并没有太大的差别,甚至有人就推崇在Java中,一个接口就只有一个方法。
=====
OOP、FP、CSP、Actor等思想,其实都是在做取舍,究竟要隐藏那些细节暴露那些功能。如果什么都不考虑,那就是汇编了(近似的说法)。没有最优的设计思想只有合适的设计思想。
无论OOP/FP/CSP/Actor模型,都是可以相互转换、替换和实现。
FP/CSP/Actor中大量用闭包,其实就是把OOP的结构体交给编译器去自动生成而已,每个闭包函数捕获的upvalues在各种支持闭包的语言中,多是交给编译生成一个特殊命名的结构体,并在闭包传递时一并生成实例并传递引用。这样就使得一些地方用于消息传递的结构体可以省略,很多时候在chan中传递一个func()比传递一个消息更加的方便。Go的chan可以看作是把传统OOP语言以 方法调用形式 表达的同步阻塞消息传递,改成了显式的消息传递,更好的是,多路分发和逆多路分发机制也集成在语言中。
OOP中的方法调用,本质上就是一种同步阻塞的消息传递,这点在ObjC中表现的非常清楚,ObjC中每个方法调用本质上就是发送了一条消息给某对象(sendmsg是一个变参数函数)。OOP隐形约定了,所有的进程内的语言运行时级别的消息传递都是同步阻塞的。而Go/Erlang等CSP/Actor模型的语言,打破了这一点,提供了语言级别的异步非阻塞的消息传递。
如果我们把软件设计分成 装配、动力驱动、可变性 三个方面考虑。
OOP的装配工作量比CSP要大的多。每个接口的每个方法都可以看成是一个螺丝,只有你紧固了每一个螺丝,OOP设计的软件才可以运行。而CSP设计的程序,每一个协程的创建,都是一个装配点,仰赖方便的闭包机制,装配所需螺丝是一次性自动紧固的。这就是CSP在设计上的优势之一吧。
在动力驱动方面,OOP由于假设了方法调用是同步阻塞的消息传递,其动力驱动也比较原始,大部分是依赖操作系统提供的线程和进程机制。但是CSP则提供了异步的非阻塞消息机制,以及自动上下文保存的可中断函数(也就是协程)。这些机制使得CSP的动力驱动简单高效。
在可变性方面,OOP的合约是由接口和结构体来约束的,而CSP的合约是由函数签名和闭包的upvalues来约束的。函数的参数和返回值可以都是空,只用upvalues来隐式表达约束。因此CSP在可变性方面也是更优秀的。
P.S.
需要强调的是OOP并没有什么特别的不好的,相反OOP具有巨大的优势,就是容易设计。
CSP虽然会要求从设计上改变即有思路,耗费较多的脑力,但其设计方案简单容易扩展,具有巨大的优势。