面向对象编程反击!
原帖发布于:大泥球
免责声明:本文包含一些幽默元素。如果您对幽默敏感,请勿继续阅读。
最近我在DZone上读到了一篇名为《开始怀疑面向对象编程》的文章。这并非我读到的第一篇在博客上用面向对象编程的名义赞扬函数式编程的文章。所有这些文章都表明,面向对象编程已经消亡了(或多或少)。我认为所有这些文章的根源在于,人们对函数式编程的真正含义存在误解。现在,我该对不同编程范式之间的争论发表一下自己的看法了。
首先,我想说,我尊重上述文章作者的观点。我接下来要说的只是我个人对这个问题的看法。对于这个困境,没有绝对正确或绝对错误的观点。所以,祝愿大家平安喜乐。
什么是函数式编程?
我认为,为了更好地理解我对函数式编程的观点,首先了解这个术语的含义非常重要。
在计算机科学中,函数式编程是一种编程范式[..],它将计算视为数学函数的评估,并避免改变状态和可变数据。
这是维基百科给出的定义。你读过任何关于Lambda 表达式的参考资料吗?没有。所以,在代码中滥用 Lambda 表达式并不意味着你正在使用函数式编程范式。
首先,你需要引用透明性,这意味着任何时候使用相同的输入调用函数都必须返回相同的输出。正确实现引用透明性的前提是状态的不变性。一旦创建了一个结构体(我们可以称之为类型吗?),它就不能以任何方式被修改。
简单吧?引用透明意味着没有副作用,不会抛出异常,也不用从外部数据源读取数据等等。如果没有这些功能,我们怎么提高效率呢?
故事中最艰难的部分
感觉我们被困在黑洞里了,不是吗?函数内部不能有副作用(其实也没那么糟糕),不能向函数调用者发出可能出错的信号,也不能访问任何外部资源。我的天哪!
你听说过幺半群(Monoid)、函子(Functor)、单子(Monad)吗?这些结构直接源自数学的一个分支——范畴论。描述这些结构超出了本文的范畴,我们来举几个例子。
我确信你的代码中至少使用了一些 Monad。如果你是 Scala 开发者,可能使用过Option[T]
类型、 或Either[T, E]
,或者任何类型的集合,例如List[T]
等等Set[T]
。如果你是 Java 开发者,你可以考虑Optional<T>
、Collection<T>
和 之类的类型Stream<T>
。所有这些类型都是 Monad。
从行为的角度来看,这些类型没有任何共同之处,这意味着 Monad 是一种在彼此不直接相关的不同类型之间共享某些属性(基本上是代码重用)的机制。这是什么意思呢?引用《Scala 设计模式》一书的话:
Monad 是一种将计算表示为一系列步骤的结构。Monad 可用于构建管道,并能将具有副作用的操作清晰地添加到一切都不可变的语言中。
让我们借用《Scala 函数式编程》这本优秀书籍中对 Monad 的定义
unit
monad 是 monad 组合子(即和)的最小集之一的实现flatMap
,满足结合律和恒等律。
单子组合子?结合律?恒等式?独角兽?什么鬼?!?我只是个普通的开发者:我了解数学,然后改变编程范式。
这篇论文我该写成什么样?真正的函数式编程其实是处理数学定律和理论的。如果你遵循这些定律来开发程序,你将受益于一系列直接源于数学理论的优良特性,例如可组合性、可测试性、线程封闭性。
然而,你需要学习数学。范畴论的点点滴滴会打湿你的脸。
面向对象编程
那么面向对象编程呢?你听说过那些必须遵循的枯燥数学定律吗?你听说过像单子、函子之类的深奥术语吗?你曾经在面向对象程序中运用过一些数学理论吗?
不。面向对象编程的美妙之处在于它几乎不需要数学知识。每个人都可以开始学习面向对象编程语言,例如 Java、C++ 或 Kotlin。乍一看,面向对象编程与我们对现实的感知非常接近。
作为生活在这个世界幸运角落的人类,我们知道每辆汽车都是由发动机、车轮、车身等等组成的。我们理解Car
类型的含义,以及为什么它拥有Engine
、Wheel
和类型的属性Body
。
面向对象编程比函数式编程更容易学习。别说了,这是唯一的真理。这两种范式或多或少都存在于计算机时代初期(比如 Lisp,它诞生于 1958 年)。你听说过用函数式编程语言编写的操作系统吗?没有。
简单性导致权衡
更简单意味着更少的限制。更少的限制意味着更少的繁琐。更少的繁琐意味着更容易以错误的方式使用编程语言的特性。
就拿我们刚才给出的单子(monad)的定义来说吧。定义中明确规定了类型必须满足哪些约束才能被视为单子。数学不会说谎。
现在,以任何面向对象编程的原则为例:例如,单一职责原则。该原则指出
一个类应该只有一个改变的原因。
他妈的,有什么理由改变?函数式编程语言的那些数学原理都去哪儿了?一点踪迹都没有。
所有面向对象编程相关理论的基础——耦合——的定义都没有得到正式定义。
组件之间的耦合度恰恰衡量了组件之间的依赖程度。
好的,那么,我该如何衡量组件之间的依赖程度呢?没有正式的方法。我之前在一篇文章《依赖关系》中尝试给出这类概念的数学定义,但那只是一次尝试。
原则定义不够严谨,容易引发对原则的解读,而个人解读往往会导致错误和不良实践。
结论
在讨论了函数式编程和面向对象编程范式之间的差异之后,我们发现并理解了一个重要的概念,即:
编程语言越容易学习,使用它就越容易犯错误。
下图试图直观地展示这句话的含义。
最后,面向对象编程语言不会消失。我们将继续使用它们,因为它们易于学习。函数式编程语言也将继续存在。每当我们需要确保程序的一些良好特性时,它们都会帮上大忙。
这个世界充满了权衡。所以,停止编程范式之间的战争,开始从双方的力量中汲取精华吧。
参考
- 维基百科上的函数式编程
- 第 11 章 Monads。Scala 中的函数式编程,Paul Chiusano 和 Runar Bjarnason,2014 年,Manning 出版社
- Monads,第 1 章。现有的设计模式和设置你的环境,Ivan Nikolov,2016 年,Packt Publishing
- 第八章:单一职责原则(SRP)。C# 中的敏捷原则、模式和实践,Robert C. Martin、Micah Martin,2006 年,Prentice Hall
- 依赖性。