函数式编程是否替代了GoF设计模式?

1147
自从去年开始学习F#OCaml以来,我已经阅读了大量的文章,这些文章坚称设计模式(特别是在Java中)是命令式语言中缺失功能的解决方法。我发现一篇文章提出了一个相当强的观点
引用:

我遇到的大多数人都读过Gang of Four (GoF)写的设计模式一书。任何有自尊心的程序员都会告诉你,这本书是与语言无关的,其中的模式适用于软件工程,无论你使用哪种语言。这是一个高贵的说法。不幸的是,它与事实相去甚远。

函数式语言非常表达力强。 在函数式语言中,人们不需要设计模式,因为语言很可能是如此高级,以至于你最终会编写消除所有设计模式的概念。

函数式编程(FP)的主要特点包括将函数作为一等值、柯里化、不可变值等等。对我来说,面向对象的设计模式似乎没有近似任何这些特性。
此外,在支持面向对象编程的函数式语言中(例如 F#和 OCaml),我认为使用这些语言的程序员会使用与其他任何面向对象语言可用的相同设计模式。实际上,现在我每天都在使用 F#和 OCaml,我在这些语言中使用的模式与我在使用 Java 时使用的模式之间没有明显的区别。
函数式编程是否消除了对OOP设计模式的需求?如果是这样,您能否发布或链接到典型OOP设计模式及其功能等效性的示例?

21
你可以看一下 Steve Yegge 的文章(http://steve-yegge.blogspot.com/2006/03/execution-in-kingdom-of-nouns.html)。 - Ralph
33
这本书不依赖于特定编程语言,所介绍的模式适用于一般的软件工程。需要注意的是,该书对此作出了异议,因为某些编程语言并不需要表达诸如设计模式之类的东西:"我们的模式假定具备Smalltalk/C++级别的语言特性,而这种选择决定了什么可以轻松实现和不能轻松实现[...]例如,CLOS具有多方法,这减少了使用Visitor等模式的必要性(第331页)"(第4页)。 - Guildenstern
7
请注意,许多设计模式在足够高级的命令式语言中甚至不是必需的。 - R. Barzell
5
@cibercitizen1 是一种支持高阶函数和匿名函数的鸭子类型语言。这些特性提供了许多设计模式所要提供的功能。 - R. Barzell
5
相关链接:http://mishadoff.com/blog/clojure-design-patterns/ - Daniel Jour
显示剩余6条评论
29个回答

1171
这篇引用的博客有点夸大其词。FP并没有消除对设计模式的需求,只是在FP语言中,“设计模式”这个术语不常被用来描述相同的事情。但是它们确实存在。函数式语言有很多最佳实践规则,形式为“当遇到问题X时,请使用看起来像Y的代码”,这基本上就是一个设计模式。
然而,大多数面向对象编程(OOP)特定的设计模式在函数式语言中几乎没有什么相关性。
总体而言,我认为可以说,设计模式存在的原因只是为了弥补语言的不足之处。如果另一种语言可以轻松地解决同样的问题,那么这种语言就不需要设计模式。该语言的用户甚至可能不知道该问题的存在,因为在该语言中,这不是一个问题。
以下是Gang of Four在这个问题上的观点:
选择编程语言很重要,因为它会影响人们的观点。我们的设计模式假设Smalltalk/C++级别的语言特性,并且这种选择决定了什么可以轻松地实现,什么无法轻松地实现。如果我们假设过程式语言,我们可能已经包括名为“Inheritance”、“Encapsulation”和“Polymorphism”的设计模式。同样,有些模式在不太常见的面向对象语言中直接支持。例如,CLOS具有多方法,这减少了像Visitor这样的模式的需要。实际上,在Smalltalk和C++之间存在足够的差异,以至于某些模式可以在一种语言中更容易地表达,而在另一种语言中则更难。(例如,参见Iterator)。
(以上是《设计模式》一书引言第4页第3段的引用)
函数式编程的主要特点包括函数作为一等公民、柯里化、不可变值等。对我来说,面向对象的设计模式似乎没有逼近这些特征。
如果不是函数式编程语言中的第一类函数的近似,那么命令模式是什么呢?在FP语言中,你只需将一个函数作为参数传递给另一个函数。在OOP语言中,您必须将函数封装在一个类中,然后实例化该对象,然后将该对象传递给其他函数。效果是相同的,但在面向对象编程中,它被称为设计模式,并且需要更多的代码。抽象工厂模式又是什么,如果不是柯里化?逐步向函数传递参数,以配置最终调用时它会产生什么样的值。
因此,几个GoF设计模式在FP语言中变得不再需要,因为存在更强大且易于使用的替代方法。但当然,仍然有一些设计模式在FP语言中没有被解决。例如,单例(Singleton)的FP等效项是什么?它也是双向的。正如我所说,函数式编程也有其设计模式;只是人们通常不会考虑它们。
但你可能已经遇到过单子。如果不是为了“处理全局状态”的设计模式,那么它们是什么?在面向对象的语言中,这是一个非常简单的问题,因此在那里不存在等效的设计模式。
我们不需要“增加静态变量”或“从该套接字读取”等设计模式,因为这就是你所做的。
认为单子是一个设计模式就像认为整数带有它们的普通操作和零元素一样荒谬。不,单子是一种数学模式,而不是设计模式。
在(纯)函数式语言中,除非你使用单子“设计模式”或其他允许相同操作的方法,否则不可能存在副作用和可变状态。
此外,在支持面向对象的函数式语言中(例如F#和OCaml),我认为使用这些语言的程序员将使用与任何其他面向对象语言中可用的相同的设计模式。实际上,现在我每天都使用F#和OCaml,并且在这些语言中使用的模式与我在Java中编写时使用的模式没有显着区别。
也许是因为你仍然在以命令式方式思考?很多人在一生中都使用命令式语言,当他们尝试使用函数式语言时,很难放弃这个习惯。(我见过一些非常有趣的F#尝试,其中每个函数基本上都是'let'语句的字符串。 :))
但另一个可能性是你还没有意识到在面向对象的语言中需要设计模式的那些微不足道的问题,在函数式语言中可以轻松解决。
当你使用了柯里化或将一个函数作为参数传递时,请停下来思考如何在面向对象的语言中完成它。
是的。当你使用FP语言工作时,你不再需要特定于OOP的设计模式。但是您仍然需要一些通用设计模式,例如MVC或其他非OOP特定内容,并且需要一些新的FP特定“设计模式”。所有语言都有其缺点,而设计模式通常是我们解决这些缺点的方法。

无论如何,你可能会发现尝试使用“更干净”的FP语言非常有趣,例如ML(至少在学习目的上是我个人最喜欢的)或Haskell,在这些语言中,当你面临新问题时,没有OOP支撑可以依靠。


正如预期的那样,一些人反对我的设计模式定义为“弥补语言缺陷”,因此这里是我的辩解:

正如已经说过的,大多数设计模式都特定于一种编程范型,有时甚至只限于一种具体语言。通常,它们解决了仅存在于该范型(请参见FP的monads或OOP的抽象工厂)中的问题。

为什么抽象工厂模式在FP中不存在?因为它试图解决的问题在那里并不存在。

因此,如果一个问题存在于OOP语言中,而在FP语言中不存在,那么显然这是OOP语言的缺陷。该问题可以得到解决,但您的语言并没有这样做,而需要您提供大量样板代码来解决它。理想情况下,我们希望编程语言可以神奇地消除所有问题。任何仍然存在的问题从本质上讲都是语言的缺陷。;)


81
设计模式描述了基本问题的通用解决方案。但编程语言和平台也是如此。因此,当您使用的语言和平台不足以满足需求时,您会使用设计模式。 - yfeldblum
142
S.Lott: 是的,它们描述了在某种语言中存在的问题的解决方案。函数式编程语言中没有命令设计模式,因为它试图解决的问题不存在。这意味着它们解决了语言本身无法解决的问题,也就是语言的缺陷。 - jalf
41
单子是一个数学概念,你在分类上有些过于牵强。当然,你可以将函数、幺半群、单子、矩阵或其他数学概念视为设计模式,但它们更像是算法和数据结构,是基本概念,与语言无关。 - Alexandru Nedelcu
45
当然,单子是一个数学概念,但它们也是一种模式。 单子的“FP模式”与单子的数学概念有所不同。前者是一种模式,用于克服纯FP语言中某些“限制”。而后者则是一种普遍的数学概念。 - jalf
77
请注意,Haskell 中的单子除了可变状态之外还用于其他许多应用,例如异常、延续、列表推导式、解析、异步编程等等。但所有这些单子应用都可以被称为模式。 - JacquesB
显示剩余46条评论

166
是否有关于函数式编程可以取代OOP设计模式的说法? 函数式编程与面向对象编程并不相同。面向对象设计模式并不适用于函数式编程,而是需要使用函数式编程设计模式。因此,在函数式编程中,你不会阅读OO设计模式的书籍,而是需要阅读其他关于FP设计模式的书籍。
是否完全是“面向语言”的? 不完全如此。它只是针对OO语言“面向语言”的。这些设计模式根本不适用于过程化语言。它们在关系数据库设计上几乎没有意义。在设计电子表格时也不适用。
是否存在典型的OOP设计模式及其函数式等效物? 上述内容本身就不存在。这就像要求将过程性代码重写为OO代码一样。如果我将原始Fortran(或C)翻译成Java,那么我所做的只是翻译。如果我把它完全重写成面向对象的范例,它将不再像原始的Fortran或C一样--已经变得面目全非了。
从OO设计到函数式设计没有简单的映射。它们是解决问题的非常不同的方式。
函数式编程(像所有编程风格一样)拥有设计模式。关系数据库具有设计模式、OO具有设计模式、过程式编程也有设计模式。所有东西都有设计模式,甚至包括建筑物的建筑结构。
作为一个概念,“设计模式”是一种无关技术或问题域的建设方式。但是,特定的设计模式适用于特定的问题域和技术。
每个思考自己在做什么的人都会发现设计模式。

16
MVC不是面向对象设计,而是架构设计 -- 这种模式应用范围相当广泛。 - S.Lott
1
@Princess:函数式编程并不一定更简单。在你的例子中,是的。对于其他事情,仍有争议。但你已经放弃了Java面向对象设计模式,采用了FP设计模式。 - S.Lott
2
+1:我比起上面Jalf的答案更倾向于这个回答。虽然一些设计模式可以解决语言的不足,但并非全部都是如此。例如,“解开递归结构”的设计模式并不能解决语言本身的不足,它只是一种有用的传统,能够减少依赖性。 - J D
9
Java 8将包括闭包,也称为匿名函数或Lambda表达式。这将使得Java中的命令设计模式过时了。这是语言缺陷的一个例子,对吗?他们增加了一个缺失的功能,现在你不需要设计模式了。 - Todd Chaffee
2
设计模式旨在简化编程,使得最终的程序更加高效,并且能够达到其预期的目标。+1 - Sorter
显示剩余3条评论

51

Brian对语言和模式之间紧密联系的评论非常到位,

这次讨论中缺失的部分是习惯用法的概念。James O. Coplien的书《高级C++》在这方面有很大的影响。早在他发现Christopher Alexander和“没有名字的柱子”(如果没有读过Alexander,你就不能明智地谈论模式),他就谈到了掌握习惯用法在真正学习一门语言中的重要性。他以C语言中的字符串复制为例:while(*from++ = *to++);你可以把它看作是一种缺失的语言特性(或库特性)的临时解决方案,但它真正重要的是它是一个比任何部分都更大的思想单元或表达单元。

这就是模式和语言所试图做的,让我们能够更简洁地表达我们的意图。思想单元越丰富,你能够表达的思想就越复杂。拥有丰富的、共享的词汇,从系统架构到位操作,都允许我们进行更加智能化的交流,并思考我们应该做什么。

作为个体,我们也可以学习。这正是这项练习的全部意义所在。我们每个人都能理解和使用那些我们自己从未想到过的东西。语言、框架、库、模式、习惯用法等等都有它们在分享知识财富中的位置。

8
谢谢!这就是模式所关注的——“概念划分”,以降低认知负担。 - Randall Schulz
2
函数式Monad在这个讨论中肯定是必不可少的。 - Greg
1
@RandallSchulz:语言特性(当然还包括它们的惯用法)也很适合归为“概念切块以降低认知负荷”的类别。 - Roy Tinker

47

《设计模式:可复用的面向对象软件元素》这本书明确地与面向对象编程保持联系 - 书名中提到了“面向对象”(我加粗了标题)。


35

1
只有四种模式被一级函数明确地排除,这是值得的。一级类型最终成为大量减少工作量的消除者(消除了六个),但也有同样大量的模式被非常非传统的Common Lisp对象系统提供的特定功能所消除,该系统实质上推广了面向对象编程并使其更加强大。 - saolof

27

4
这篇文章似乎并没有真正展示Haskell中的设计模式,而是展示了Haskell如何在没有使用这些模式的情况下处理这些需求。 - Fresheyeball
4
这取决于你对模式的定义。将一个函数映射到一个列表上是否是访问者模式的一种变体?我通常认为答案是“是”。模式应该超越特定的语法。被应用的函数可以作为对象包装或作为函数指针传递,但在我看来,这个概念是相同的。你不同意吗? - srm

24

当你试图从“设计模式”(总体上)和“函数式编程与面向对象编程”的角度来看待这个问题时,你会发现答案最多只是模糊的。

但如果从更深层次的角度来考虑,比如考虑具体的设计模式和语言特性,事情就会变得更加清晰。例如,一些特定的设计模式,比如访问者模式、策略模式、命令模式和观察者模式,在使用带有代数数据类型和模式匹配、闭包、头等函数等语言特性的语言时肯定会发生改变或消失。但另一些来自 GoF 书籍的模式仍然“存在”。

总的来说,我认为随着时间的推移,新的(或者正逐渐流行起来的)语言特性正在逐步淘汰某些特定的设计模式。这是语言设计的自然过程;随着语言越来越高级,以前只能在书中用示例呈现的抽象概念现在变成了特定语言特性或库的应用。

(附言:这里有一篇我写的最近的博客,其中还有其他关于函数式编程和设计模式的讨论链接。)


1
你怎么能说访问者模式“消失”了呢?它不仅仅是从“创建一个带有一堆Visit方法的Visitor接口”变成了“使用联合类型和模式匹配”吗? - Gabe
24
是的,但这已经从一种你可以在书中阅读并应用于代码的设计理念——模式,变成了“只是代码”。也就是说,在这种语言中,“使用联合类型和模式匹配”只是你通常编写代码的方式。(类比:如果没有任何一种语言有“for”循环,它们都只有“while”循环,那么“for”可能是一种迭代模式。但是当“for”仅仅是被语言支持的结构并且人们通常就是这样编码时,它就不是一个模式了——你不需要一个模式,它只是代码而已。) - Brian
4
换句话说,“是否为设计模式”的一个不错的检验方法是:把这种写法的代码展示给一名大二计算机专业、具有一年编程经验的本科生。如果他们看了之后说“这是个巧妙的设计”,那么它就是一个设计模式。如果他们看了之后反问“这不是显而易见吗?”那么它就不是一个设计模式。(如果你把这个 “Visitor” 展示给任何一个学习过 ML/F#/Haskell 一年的人,他们会觉得这很显然。) - Brian
3
Brian:我认为我们对“模式”的定义不一样。我认为任何可识别的设计抽象都是“模式”,而你只认为非显而易见的抽象才是“模式”。仅仅因为 C# 有 foreach,Haskell 有 mapM 并不意味着它们没有迭代器模式。我认为在C#中,可以说迭代器模式被实现为通用接口 IEnumerable<T>,Haskell中则以类型类Traversable的形式存在,这样做并没有问题。 - Gabe
1
也许对于软件工程师来说,非显而易见的模式可能很有用,但所有模式对于语言设计者都是有用的。即,“如果你正在创建一种新语言,请确保包含一种清晰的表达迭代器模式的方式。” 即使是显而易见的模式,在我们开始问“是否有更好的语法来表达这个想法?”时也很有趣。毕竟,这就是导致某人创建foreach的原因。 - srm

18

我认为,当您使用具有宏支持的Lisp这样的语言时,您可以构建自己的特定领域抽象,这些抽象通常比一般惯用语解决方案更好。


我完全迷失了。用抽象的方式提升某些东西...这是什么意思? - tuinstoel
3
你可以构建特定于领域的抽象(甚至是嵌入式的),而无需使用宏。宏只是让你通过添加自定义语法来美化它们。 - J D
2
你可以把Lisp看作是一组用于构建编程语言的乐高积木 - 它不仅是一种语言,还是一种元语言。这意味着对于任何问题域,你都可以定制设计一种没有明显缺陷的语言。虽然需要一些练习,而且库尔特·哥德尔可能会有不同看法,但花点时间学习Lisp是值得的,因为它带来了令人惊叹的宏功能。 - Greg
JD:这有点像说你总是可以写汇编语言一样。宏可以进行相当复杂的代码转换。在其他语言中,您可以在技术上构建和操作DSL的AST,但是宏使您可以使用所使用的语言的普通表达式来执行此操作,以便DSL可以更清晰地集成。 - saolof

17
Norvig的演示提到了他们对GoF模式进行的分析,称其中有16个模式在函数式语言中有更简单的实现方式或者直接是这种语言的一部分。因此,至少有7个模式可能同样复杂或者根本不存在于该语言之中。但遗憾的是,他们没有详细列举。
我认为很清楚,大多数GoF中的“创建”或“结构”模式只是技巧,用于让Java或C++中的原始类型系统按照你的意愿工作。但其他模式无论在哪种编程语言中都值得考虑。
例如,“原型”模式可能就是其中一个;尽管它是JavaScript的基本概念,但在其他语言中必须从头开始实现。
我的其中一个最喜欢的模式是“空对象”模式:将某物的缺失表示为一个什么也不做的适当类型的对象。这在函数式语言中可能更容易建模。然而,真正的成就在于思维模式的转变。

2
这是一个奇怪的分析,因为GoF模式是专门为基于类的面向对象编程语言设计的。这有点像分析管钳是否适用于做电工工作。 - munificent
1
@munificent:不完全是这样。面向对象提供多态性;函数式编程通常提供多态性。 - Marcin
1
@AndrewC 我不同意。面向对象的程序员可能认为它们意思不同,但实际上并不是这样。 - Marcin
3
根据我的经验,面向对象编程者通常指的是子类型多态性(通常只使用Object),利用强制转换来实现它,或者是特定场景下的多态性(如重载等)。当函数式编程者谈论多态性时,他们指的是参数多态性(即适用于_任何_数据类型 - Int、函数、列表),这可能更类似于面向对象的泛型编程,而不像面向对象程序员通常所说的多态性。 - AndrewC
1
@AndrewC 好的,是的。我的观点是,所有足够好的面向对象系统都至少提供一种参数多态类型。我发现将模板引入C++中对编程风格产生了巨大影响,以至于通过模板实现的参数多态已经超过了类的使用。这使得语言更加优美。 - Marcin
显示剩余2条评论

9

即使是面向对象的设计模式解决方案也是与语言相关的。

设计模式是解决编程语言没有为您解决的常见问题的解决方案。在Java中,单例模式解决了某些情况下只需要一个实例(简化)的问题。

在Scala中,除了类Class之外,还有一个名为Object的顶层构造。它是延迟实例化的并且只有一个实例。您不必使用单例模式来获取单例。它是语言的一部分。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接