编程是一门艺术,就像它是一门科学一样。个人偏好在决定你的编程风格方面起着很大的作用,所以你可能不会惊讶地发现自己与同行的争论。一个正在进行的辩论是在两种不同的编程范式之间的选择,即函数式编程和面向对象编程。哪一个更好?你应该使用哪一个?
任何一方的支持者都会告诉你,他们最喜欢的范式提供了一些明显的优势,几乎是普遍适用的。你可以争辩说这取决于。在这篇文章中,我们将看看这两种选择,并试图从事实中提炼出神话。其目的是为了更好地理解这些编程方法,并准备在有意义的时候使用它们。
功能性编程
函数式编程(FP)是最古老的编程方式之一,甚至可能是最古老的。它定义了一个完全依赖函数的软件构建过程。在FP中,开发者通过组合函数来创建新的函数,并编写应用程序以避免共享状态或可变数据等方面。为了实现这一点,开发人员经常以声明的方式而不是以命令的方式使用FP。
那么,这一切意味着什么呢?
函数式编程的关键概念
首先,重要的是要理解,函数在FP中是一等公民,这意味着它们被当作任何其他的值来对待。它们可以作为参数被传递,也可以从其他函数中返回。一个能返回一个函数的函数被称为高阶函数。这些高阶函数使得直接从参数构成新函数成为可能。
第二个要点是不可变性的概念。一个不可变的值是一个原始的值,一个不能改变的值。像数字这样的值被认为是不可变的。你不能将42改为14。你只能创建一个数值为14的新数字,并将其分配给先前使用的变量。但让这个新数字变成14并不影响你之前使用的数字42。
虽然这种数值处理方式对于数字等基元来说是有意义的,但对于对象或数组等组成的数值来说,可能会感觉很奇怪。然而,FP原则将所有的值都视为不可改变的。改变一个值的唯一方法是创建一个新的,有可能使用一个旧的作为基础值或副本。
使用不可变的数据类型使得FP可以使用纯函数。这些函数仅由其参数定义。由于参数不能改变,它们的行为保证是可预测的。相同的参数会产生相同的结果。其他编程方法不能保证这种可预测的行为。
函数式编程的使用案例
虽然它最初主要用于科学应用,但FP在各个领域都变得越来越流行。例如,在网络开发中,一个名为React的用户界面(UI)库利用FP原则,使之成为声明性的,易于处理的。该库主张使用不可变的状态对象(值)来反映应用程序的当前状态而不改变。当开发者想要一个新的状态时,他们必须创建一个新的对象。这种方法的好处是,你可以追溯到用户界面的每一个变化。所有以前的状态仍然没有被触动,而且是可用的。
虽然FP的根基肯定是在计算机科学的学术方面,但其实际意义可以追溯到Lisp或Scheme等语言。其中一些特征被烘烤成了JavaScript语言,这是FP继续使用的一个关键因素。然而,有些语言是建立在FP原则之上的。F#、Clojure和Elixir都是比较流行的语言。在学术界,Haskell几十年来一直被认为是常用的语言。
面向对象的编程
函数式编程已经存在了很长时间,但有些开发者认为面向对象编程(OOP)甚至更加传统。像Smalltalk或Objective-C这样的编程语言普及了OOP,它是在1970年代末发明的。后来,C++、Java和C#继续将这种编程风格硬塞进大多数开发者的脑海中。
继续阅读,了解OOP中的重要原则和一些最常见的使用案例的描述。
面向对象编程的关键概念
在OOP中,开发人员将软件应用程序建模为可以相互通信的对象的集合。每个对象的接口是一个类,一个表明任何实例都可以访问功能和值的模板。虽然这种通信能力最初被设想为允许网络通信或一般异步IO的消息传递,但它后来被普及为在相应对象上调用的简单函数。这样的函数被称为方法。
与FP中不可变的对象相比,在OOP中,对象的变异是游戏的一部分。因此,调用一个方法很可能也会改变对象的某些值。
许多开发者仍在使用OOP,特别是在教授编程时,原因之一是它是强制性的编写。这意味着开发者很明确地知道在哪里发生了什么。反驳的理由是,即使使用这样的命令式风格,也很难确定单个对象的当前状态。由于易变性,这可能很快导致不可预见的后果。
面向对象编程的用例
传统上,开发人员用OOP的方式制作几乎所有的UI。这也是相对简单的,因为一个基于类的组件可以直接从另一个类似的组件中继承其基本结构(字段和方法)。例如,一个日期输入字段可以继承于一个文本输入字段,而文本输入字段又继承于一个输入框,而输入框又继承于一个控件。使用这种基于继承的方法,你只需要指定额外的方法和重新实现一些现有方法的行为。例如,没有必要再次编写键盘或鼠标处理的逻辑。
今天,OOP是所有通用编程语言的必备功能。甚至基于FP的语言,如F#,也直接支持OOP特性,如类或继承。一个很好的例子是JavaScript,它没有立即纳入类或继承等标准功能,而是在最近的修订中加入了这些功能。
正如我们所发现的,这两种编程方法的不同之处足以证明在适用的情况下使用这两种方法。那么,你如何在它们之间做出选择呢?下面是一些争论。
功能性编程与面向对象编程:辩论
正如已经讨论过的,两种方法都有其好处和缺陷。计算机科学教授Norman Ramsey在一个著名的Stack Overflow回答中提供了一个有用的观点。他认为,当所有对象都是已知的,但其行为可能会发生变化时,FP就会表现出色。相比之下,当行为是已知的,但实际的数据类型可能会改变时,OOP就很出色。
两边的粉丝都会超越这个范围。例如,FP的追随者认为,以纯FP风格编写的设计简洁的软件很容易调试,而且永远不会崩溃。他们告诉你,FP能让你立即获得测试驱动开发的一些优势,而严格应用所有SOLID原则的OOP本质上就是FP。虽然SOLID确实会导致个别功能类似于大部分的FP,但它不一定是FP。例如,没有一个SOLID原则禁止数据突变。
热爱OOP的开发者可能会因为性能或简单性等方面的权衡而放弃一些FP的好处。当你只想改变一个字段时,为什么要把一个对象的所有字段复制到一个新的对象中?为什么一个有一百万个元素的数组需要通过复制来设置一个元素?当然,你可以用一些模式来避免复制,但这样你又需要专门的数据结构。与独立于OOP之外的开发人员长期以来使用的直接方法相比,这里的间接性水平可能令人困惑。
虽然这些方法之间存在着一些相当鲜明的对比,但它们也可以是互补的。没有任何法律禁止在软件应用中使用类。也没有法律规定应用程序的一般状态应该是可变的。
展望未来
几乎所有流行的编程语言都是多范式的。它们都支持FP,允许函数传递,或有处理数据对象不变性的帮助器。它们还带有OOP功能,如类或继承。无论是哪种方式,这种多范式的方法都将继续存在。从用户的角度来看,有更多的选择几乎总是更好的。
一般来说,似乎有可能出现对FP友好的功能的趋势。通过引入对克隆数据对象、函数引用和函数组合的额外支持,一种语言会成为一个伟大的伴侣,甚至超越FP。在诸如C#这样的语言中,对FP友好的特性为OOP特性提供了一个很好的补充。这种组合甚至在OOP开发的应用程序中也很方便,比如当你使用某些辅助函数时。
混合FP和OOP方法的缺点是学习曲线。有些人因为这个原因而避免使用C#。一开始是一个原始的、设计优雅的Java的替代品,最后却感觉更像是一个类似于C++的复杂怪物。
OOP当然不会消失,但最有可能的结果是,它将找到与FP共存的位置。诚然,无副作用开发的理想在实践中几乎不可能实现。在向控制台写入日志信息时已经有了副作用。开发者们必须首先发现并解开FP的实际一面。现在,开发者已经开始认识到FP的实用性,几乎没有理由再把FP的功能锁定。
对面向对象编程的持续支持
在这种情况下,为什么OOP仍然是一个可行的选择?OOP有价值,因为它本质上是理想的工具。几乎没有什么是开发人员不能通过OOP实践来建模的。开发人员甚至可以通过OOP对大多数FP功能进行建模。
例如,考虑像传递或组合函数这样简单的东西。简而言之,这就是函数或委托提供的东西。它只是一个对应于具有单一方法的类的对象。
寻找共同点
很少有开发团队会为了从一种方法转换到另一种方法而改变他们的软件或编写应用程序的方式。更有可能的是,一个团队会积极地重构他们的一些最基本的应用程序,以使用他们的编程语言的最新功能。
无论哪种方式,未来看起来越来越混合。在未来的某个项目中,你可能会发现自己在OOP应用中使用了更多FP启发的风格,或者在FP驱动的应用中使用了一些OOP特性。
总结
某些类型的应用偏爱FP(如编译器),而另一些则更适合使用OOP原则(如经典的桌面应用)。选择正确的编程方法取决于几个因素,如目标编程语言、框架和你正在构建的应用程序的类型。使用你觉得最舒服的方法,但不要忘记继续学习以增加你的工具箱。在解决一个问题时,有很多选择总是有益的。