我有个想法,可是好一阵子都犹豫不决,因为我觉得它是错的。它只是过于通用,以至于令人难以置信,尽管我承认我的观点略显抽象,不过我认为……


我有个想法,可是好一阵子都犹豫不决,因为我觉得它是错的。它只是过于通用,以至于令人难以置信,尽管我承认我的观点略显抽象,不过我认为其中另有玄机。就从这里开始吧!

Michael Feathers

面向对象(object-orientation)更适合更高的系统层次,而函数式编程(functional programming)更适合较低的系统层次。

有趣的想法,不过我们又该如何实现呢?

好吧,对我而言,这就回到了那些方法的基本原理。

函数式编程(functional programming)

functional programming

尽管函数式编程风格五花八门——定义也是千差万别,但万变不离其宗:通过减少副作用,以便我们编程时更顺手。一旦没有了副作用,你就可以更轻松地推理代码。你还会拥有引用透明性(referential transparency)——即能够在程序中把某个表达式从一处粘贴到另一处,并确信对于给定的相同输入,都将产生完全相同的输出,且不会在其他地方导致讨厌的副作用。相应的技术名称是“纯洁性”(purity)。

纯洁性(purity)不仅使代码理解起来更轻松,而且使惰性计算(lazy evaluation)成为可能。以防你从前对此一无所知,因此我们先来扫一下盲,在一些函数式编程语言中没有“调用函数”(calling a function)的说法,而是说“应用此函数”(apply the function)。这不只是命名原则的区别。在Haskell中,表达式 [1..10] 的计算结果为 1,2,3,4,5,6,7,8,9,10。如果把 take 函数应用于该序列,并提供参数 5take 5 [1..10]),那么你会得到 1,2,3,4,5,即该序列的前5个元素。

当在Haskell中计算 [1..] 时,你觉得结果如何?对极了,你会得到从1开始的无限整数序列。在交互式提示符下,你必须在某一时候键入 Ctrl^C 来终止打印。既然如此,那么下面这个表达式是何结果?

take 5 [1..]

你可能认为,此表达式也会永远执行下去。毕竟,在把 [1..] 传递给 take 函数前,必须先对其进行计算,而且算起来就永远不停。不过,由于惰性计算(lazy evaluation)的缘故,这种情况并不会发生。之所以说你没在调用 take 函数,是因为你正在通过函数应用产生一个表达式。当你计算整个表达式时,运行库只执行用于返回首个结果1所需的子表达式的计算,接着对第二个结果进行计算,一直算到上限5为止。

惰性计算(lazy evaluation)可能非常强大,不过关键在于:只有获得纯洁性(purity)的支持,惰性计算才能彻底启用。如果你想对此一探究竟,那么请想象有个巨大的函数表达式,且副作用深藏其中。究竟何时会出问题呢?确实无可奉告。因为这取决于被计算表达式的上下文。而在理想情况下,你根本无需关心此类问题。

面向对象(object-orientation)

object-orientation

面向对象也有某些类似的可用性。同理,面向对象的定义也是种类繁多,不过我想回到Alan Kay的最初构想上来。因为毕竟是他发明了这个术语。

Alan Kay把对象视为创造复杂系统的一种方式,且此方式非常符合大自然处理生物学复杂性的方式。在有机体中,不仅有许多细胞,而且细胞彼此之间通过传递化学信息进行通信。Smalltalk使用“消息发送”(message send)的说法而非函数调用(function call)不仅仅是巧合。对象结构的绝妙之处就在于,它并不突出玩家,而是最大化游戏本身。正如Kay所暗示的,消息比对象更重要。在生物系统中,这点会发展到全身冗余,即你无法通过杀死单个细胞来打垮整个有机体。在软件领域中,我们所掌握的与此最接近的就是Erlang进程模型,它被比作理想的对象系统。

在21世纪初,Dave Thomas和Andy Hunt曾撰写过关于被其称为“告知,不要请求”(Tell, Don’t Ask)的设计指南的文章。其思想是,只需要向对象分派任务,而不是向它们索取数据,然后由你亲自加工处理数据,这样的对象才是最棒的。不仅从封装(encapsulation)的角度看,此观点简直完美之极。而且使对象用户更简便地使用对象。一旦你取回了数据,你就必须了解它,并对它进行操作。与告知对象运用自身数据做某事比起来,这可不那么简单。

如前所述,在生物学中,细胞之间会使用化学消息。但是,在一个非常重要的方面,它们不同于典型的面向对象——细胞之间的通信是异步的。在细胞接收到某个响应以前,其任何内部活动都不会受阻。而对象系统却通常不是这样做。我们会给另一对象发送消息,然后等待响应。一旦我们得到含有返回值(数据)的响应时,便会如坐针毡。因为我们必须对它做点儿什么,或者选择忽略它——控制流再次回到我们手中。当你执行同步调用时,最终违反“告知,不要请求”简直是易如反掌。因为同步调用通常都有返回值,终究,返回值都是隐式“请求(ask)”的结果

如果我们像细胞一样来看待面向对象,那么似乎我们所用的大部分技术都有毛病。在某种编程语言中,我们可能会拥有类和对象,但只要执行同步调用,那些部件就无法像它们本来那样独立。存在比我们所谓的面向对象更加面向对象的技术么?是的,确实存在。信息技术架构(IT architectures)中的消息系统几乎都获得了此独立级别。

混搭

到目前为止,我们已看过面向对象设计和函数式编程。我觉得它们之间真是齐头并进、互不干扰。

在面向对象中,最好使用告知(tell)。一旦你使用告知(tell)的方式,你就最大限度地实现了实体间的解耦(decoupling)。如果你想防止再次耦合(re-coupling),那么你就要发送异步消息。告知(tell)模型使这一切成为可能。而在函数式编程中,最好使用请求(ask)。事实上,在纯函数式编程中也别无他法可言。不返回任何内容的函数是没有意义的,除非它有某种副作用,而且我们应该避免此类情况发生。可以采用与面向对象启用异步(asynchrony)相同的方式来让函数的纯洁性启用惰性(laziness)。

现在,如果我们接受这些前提,那么当我们组织系统时,使用何种方式最有利?我们可以在顶层放置函数层(functional layer),其职责是,当计算表达式时,在内部执行消息发送,不过这可能对系统理解造成麻烦。此外,可能会在函数层之外产生副作用,并破坏函数层的纯洁性。

反方向会怎么样?如果我们将对象层(object layer)置于顶层,并允许对象使用下层函数片段,那么又会发生什么情况?毫无疑问可以这样做,千真万确!无副作用的函数是处理内部机制的理想方式。而面向对象是高层的绝妙之选,在那里实现的解耦和信息隐藏都是至关重要的。

因此,这就是本文的观点。并且我知道此观点未必永远“正确”。像Scala等一些语言允许程序员在任何抽象级别上把对象和函数随意组合。你可以明确拥有那些用于选择和筛选对象的函数。还有微软的LINQ技术就是位于对象之上的函数层。尽管如此,我认为我的观点仍具有一些真实性。面向对象的初衷与实践中的面向对象可谓大相径庭。或者换言之,在我们现有的面向对象技术中,至少在现代软件架构的服务和消息级别上确实要更接近面向对象的初衷。在服务和消息的抽象级别上,这似乎是正确的——最好上层告知,下层请求。

查看英文原文:Tell Above, and Ask Below - Hybridizing OO and Functional Design

作者简介

enter image description here
Michael Feathers是世界级面向对象技术专家,以丰富的软件项目开发经验著称。目前在世界顶尖的软件咨询公司Object Mentor从事敏捷方法/极限编程、测试驱动开发、重构、面向对象设计、Java、C#和C++等方面的培训和项目指导。他是著名测试框架CppUnit和FitCpp的开发者,已经主持了三次面向对象界盛会OOPSLA上的CodeFest比赛。他还是《修改代码的艺术》(Working Effectively with Legacy Code)一书的作者。

iTran乐译

iTran乐译参加活动,读好文章!