为什么使用 Scala 是每一个采用 Scala 编程的程序员都需要回答的问题, 这个题目非常适合作为第一篇文章。

初次尝试

我是 2011 年的时候初次接触 Scala。当时有些空闲时间,想了解一些函数式编程,于是买了 Programming in Scala 2nd edition 的书看。大概几个月的业余时间看书学习,经历了一个从充满热情到排斥的过程。究其原因,还在于 Scala 是个多范式 (multi-paradigm)的编程语言,而且面向对象与函数式并重,甚至更偏重面向对象的概念。无论是面向对象还是函数式编程,本身都是很大很复杂的概念,混合到一起产生了像协变(covariance) 与 逆变(contra variance)这样的复杂概念。即使最基础的概念,比如函数(function),其实现也基于对象和类来实现,牵涉到的概念包括函数类(function type),匿名函数(anonymous function),方法值(method value),方法类(method type), 函数声明(function declaraation),特质(trait)等。看了 Difference between method and function in Scala 的答案,反而引出了我更加不解的问题 What are the definitions of named method and named function? 。 2011 年的结论就是 Scala 语言的学习难度应该数倍于学习单纯的面向对象编程或函数式编程,不适合作为业务应用软件团队的开发语言。

后来有机会看了一下(没有做实际项目)Haskell 以及 FSharp,其简洁和较短的学习曲线更加确认了我的想法。当然还需要考虑到语言的实用性与生态系统。FSharp 在与 OO 可以互操作的基础上加入了很多简单实用的功能比如变量、副作用以及原生的 Sum Type。FShpar 因而成为第一备选,而 Scala 则被列入不予考虑的黑名单。

从业务出发

转眼到了 2019 年,函数式与异步响应式编程已经是大势所趋,这些技术在前端得到广泛应用。我们开始认真考虑是否尝试开发新的应用体系结构。任何技术系统都是为其业务服务。技术是达成业务目标的手段,所以首先要理解我们的业务系统及相应的应用系统需求。我们的业务有不同形态的客户群体,不同形态的产品,复杂多变的业务流程。产品、客户、业务功能需要灵活的组合并且经常变。即使同一个处理流程,不同的客户也需要各种定制功能。

传统的做法是编写一套处理流程,包含所有业务处理任务,每个客户群体,甚至每个客户通过参数定制。再加上产品维度,这种做法会让主流程非常复杂,参数配置会非常多。结果就是不堪重负,难以维护,运行缓慢。

一个理想的系统的架构就是以尽可能靠近物理世界的方式运作。这样一个系统比较高效而且也容易理解。现实生活中业务系统在时空上都是以一种总体异步,局部同步的多个体并行方式在工作。特定时空的业务内容包含业务处理和数据两个维度。从数据的角度,这篇 2004 年星巴克不用二阶段提交 的博客揭示了业务数据的本质:异步、弱序列、灵活错误处理(放弃/重试/补偿)、最终一致性。 近些年分布式系统的趋势回应了类似我们业务系统需求的特点。大趋势是采用异步、分布式并发的工作方式。事物处理也大都是采用最终一致性的工作方式,放弃强一致性换来高并发和高可靠。

业务的处理流程更需要灵活的架构。不同于传统做法,采用类似于数学的分形(fractal)思想,把系统功能分成可以组合的细颗粒任务,然后以一种一致的方式对不同客户/产品进行任意组合。结果就是一种基于工作流(workflow)的工作方式。沃尔玛的 Jet 研发团队博客 Microservices to Workflows 给出了一个很好的例子。

我们团队一直采用 Java 和 Spring Fraemwork 开发一个庞大的业务应用软件。开发新的 workflow 应用架构,我们有二个选择:继续使用 Java 或 改用 FSharp 开发新系统。 基于 FShapr 的异步流处理框架只有 Rx.Net,比较低级而且人员难招,只好忍痛割爱这么好的一个函数式编程语言。初步决定继续用 Java,流行的基于 JVM 的异步流处理框架有 RxJava, Spring Reactor, Vert.x, Ratpack 和 Akka Streams。

走向 Scala

很容易就发现基于 Akka Actor 的 Akka Streams 的成熟度和架构远远领先于其它几个异步流处理框架。一旦决定基于 Akka Streams 作为未来的架构基础,就发现必须拥抱整个 Akka 生态圈,特别是 Play Framework, Akka Actor 以及 Akka HTTP 等。但是用 Java 作为开发语言的尝试发现很多问题。一个基于 Java 的 简单 Rest API 流服务尝试就踩了很多坑,其中一些坑是版本不兼容等无法简单修复的错误。这种情况让人不能不把 Scala 纳入重新考虑的视角。

与此同时,团队从一开始阴差阳错采用 Spring JPA 和 Hibernat 做数据库访问层。 对象关系映射(ORM) 的弊端臭名昭著:ORM 是计算机科学的越战泥潭。作为验证,我们跟踪了一下 JPA 程序的运行,一个稍微复杂的数据库读写比用底层 JDBC 访问至少慢一个数量级。对系统的改造,需要采用一种更高效的访问方式比如 JOOQ 或 JDBC Template。相比较而言,函数关系映射(FRM)则比较自然而且高效。基于 Scala 的 Slick 是很多年的成熟 FRM 框架,给 Scala 加分不少。

那就试试 Scala 何妨?一旦开始看文档和跑一些示例,发现 Scala 异常的方便,而且因为是原生支持,编程都比较顺利,尤其是函数式编程语言比较适合异步流处理流程。如果用 Java 或 FSharp 开发一套类似于 Akka Streams 或 Slick 这样的框架,技术上虽然可行,但是时间上远远超过学习 Scala 的时间。鉴于团队开发人员的学习能力和稳定性都好(这二点很重要,招人非常难),那就用 Scala 吧。当然,用一种实用的 Scala 编程模式。

实用 Scala 编程

程序员作为问题的解决者,都比较实实在在,倾向于采用一种实用的(pragmatci)编程方式。根据业务的异步、分布、实时性特点,函数式编程比较自然,有比较大的优势。总体上,除非对象和可变数据结构有非常突出的优势而且其影响范围可控,我们通常采用函数式编程的开发理念:纯函数、不可变数据结构、不用循环、避免异常 (Exception)、避免副作用 (side effect)等。具体到 Scala,体现在下面这些方面:

  • 尽量分隔数据和处理(函数),不要用对象把它们混到一起。
  • 不在应用程序定义类继承(Class Inheritance),必要时用 type class。
  • 不用依赖注入(Depedency Injection), 用 thin cake pattern
  • 隔离有副作用的模块和纯函数模块,采用三明治结构,即:输入 –> 纯函数处理 –> 输出

在探索实用 Scala 编程的路上,总结出一些范式来减少学习和开发成本至关重要。希望这是让团队技术升级成功的光明大道。