函数式编程是否会取代GoF设计模式?

自从我去年开始学习F#OCaml以来,我已经阅读了大量的文章,这些文章坚持认为设计模式(尤其是在Java中)是解决命令式语言中缺失的特性的变通方法。

我见过的大多数人都读过设计模式书的帮派四(GoF)任何有自尊的程序员会告诉你这本书是语言不可知论者和模式应用于软件工程中一般,无论哪种语言你使用。这是一个高尚的主张。不幸的是,它远离真相

函数式语言非常在函数式语言中表达性强不需要设计模式因为语言可能是如此之高级别,你最终会在消除设计的概念所有的模式都在一起。

函数式编程(FP)的主要特性包括作为一等值、柯里化、不可变值等的函数,在我看来,OO设计模式是否接近这些特性并不明显。

此外,在支持OOP的函数式语言(例如F#和OCaml)中,对我来说很明显,使用这些语言的程序员会使用其他OOP语言可用的相同设计模式。事实上,现在我每天都使用F#和OCaml,我在这些语言中使用的模式与我在Java中使用的模式之间没有显着差异。

函数式编程消除了对OOP设计模式的需求这一说法是否属实?如果是这样,您是否可以发布或链接到典型的OOP设计模式及其功能等价物的示例?

122110 次浏览

函数式编程消除了对OOP设计模式的需求这一说法是否属实?

函数式编程与面向对象编程不同。面向对象的设计模式不适用于函数式编程。相反,您有函数式编程设计模式。

对于函数式编程,您不会阅读OO设计模式书籍;您将阅读有关FP设计模式的其他书籍。

语言不可知论者

不完全。只是与OO语言无关。设计模式根本不适用于过程语言。它们在关系数据库设计上下文中几乎没有意义。它们在设计电子表格时不适用。

典型的OOP设计模式及其功能等价物?

这就像要求将一段过程代码重写为OO代码一样。嗯……如果我将原始的Fortran(或C)翻译成Java,我除了翻译它之外什么也没做。如果我完全将其重写为OO范式,它将不再看起来像原始的Fortran或C——它将无法识别。

从OO设计到功能设计没有简单的映射。他们看待问题的方式非常不同。

函数式编程(就像所有风格的编程)有设计模式。关系数据库有设计模式,OO有设计模式,过程式编程有设计模式。一切都有设计模式,甚至建筑物的架构。

设计模式——作为一个概念——是一种永恒的构建方式,与技术或问题领域无关。然而,特定的设计模式适用于特定的问题领域和技术。

每个思考自己在做什么的人都会发现设计模式。

我认为每种范式都有不同的目的,因此不能以这种方式进行比较。

我没有听说GoF设计模式适用于所有语言。我听说它们适用于所有面向对象语言。如果您使用函数式编程,那么您解决的问题领域与OO语言不同。

我不会使用函数式语言来编写用户交互界面,但是像C#或Java这样的面向对象语言会使这项工作更容易。

Peter Norvig的动态规划中的设计模式对这个一般主题进行了深思熟虑的报道,尽管是关于“动态”语言而不是“功能”(有重叠)。

正如其他人所说,有特定于函数式编程的模式。我认为摆脱设计模式的问题与其说是切换到函数式的问题,不如说是语言特征的问题。

看看Scala是如何取消“单例模式”的:你只需声明一个对象而不是一个类。另一个功能,模式匹配,有助于避免访问者模式的笨拙。在这里查看比较:Scala的模式匹配=类固醇上的访客模式

Scala和F#一样,是OO功能的融合。我不知道F#,但它可能有这些功能。

闭包存在于函数式语言中,但它们不必局限于它们。它们有助于委托者模式。

还有一个观察。这段代码实现了一个模式:它是如此经典,如此基本,以至于我们通常不会将其视为“模式”,但它肯定是:

for(int i = 0; i < myList.size(); i++) { doWhatever(myList.get(i)); }

像Java和C#这样的命令式语言采用了本质上是函数式的结构来处理这个问题:“foreach”。

你引用的博客文章有点夸大了它的说法。FP并不需要设计模式。“设计模式”一词在FP语言中并没有广泛用于描述同样的事情。但它们存在。函数式语言有很多最佳实践规则,例如“当你遇到问题X时,使用看起来像Y的代码”,这基本上就是设计模式。

然而,大多数特定于OOP的设计模式在函数式语言中几乎无关紧要,这是正确的。

我不认为说设计模式一般的存在只是为了弥补语言中的缺点是特别有争议的。如果另一种语言可以简单地解决相同的问题,那么另一种语言就不需要为它设计模式。该语言的用户甚至可能不知道问题存在,因为,好吧,这在该语言中不是问题。

以下是四人帮对这个问题的看法:

编程语言的选择很重要,因为它会影响一个人的观点。我们的模式假设Smalltalk/C++级语言的特性,而这种选择决定了什么可以轻松实现,什么不能轻松实现。如果我们假设是过程语言,我们可能会包括称为“继承”、“封装”和“多态”的设计模式。类似地,我们的一些模式直接得到不太常见的面向对象语言的支持。例如,CLOS有多方法,这就减少了对Visitor等模式的需求。事实上,Smalltalk和C++之间有足够的差异,这意味着一些模式可以用一种语言比另一种语言更容易地表达。(例如,参见迭代器。)

(以上引用自《设计模式导论》一书,第4页,第3段)

功能的主要特点编程包括作为一流的价值观,咖喱,不可变的值,等等。似乎很明显,OO设计模式正在接近其中任何一个功能。

如果不是第一类函数的近似值,命令模式是什么?:)在FP语言中,您只需将一个函数作为参数传递给另一个函数。在OOP语言中,你必须将函数包装在一个类中,你可以实例化该类,然后将该对象传递给另一个函数。效果是一样的,但在OOP中,它被称为设计模式,它需要更多的代码。如果不是柯里化,抽象工厂模式是什么?一次将参数传递给一个函数,以配置当你最终调用它时它会吐出什么样的值。

所以,是的,一些GoF设计模式在FP语言中变得多余,因为存在更强大和更易于使用的替代方案。

但是当然,仍然有一些设计模式是由FP语言解决的。FP相当于单例的是什么?(暂时忽略单例通常是一种可怕的模式。)

它也是双向的。正如我所说的,FP也有它的设计模式;人们通常不这么认为。

但是你可能遇到过monad。如果不是“处理全局状态”的设计模式,它们是什么?这是一个在OOP语言中非常简单的问题,以至于那里不存在等效的设计模式。

我们不需要“增加静态变量”或“从该套接字读取”的设计模式,因为它就是您的

说monad是一种设计模式就像说整数及其通常的操作和零元素是一种设计模式一样荒谬。不,monad是数学模式,不是设计模式。

在(纯)函数式语言中,副作用和可变状态是不可能的,除非您使用monad“设计模式”或任何其他允许相同事物的方法来解决它。

此外,在函数式语言中支持OOP(例如F#和OCaml),对我来说似乎很明显使用这些语言的程序员会使用相同的设计模式发现可用于所有其他OOP语言。事实上,现在我使用F#和OCaml每天,没有之间的显著差异我在这些语言中使用的模式与我写的时候用的模式Java

也许是因为你还在用命令式思维?很多人,在与命令式语言打交道了一辈子之后,当他们尝试函数式语言时,很难放弃这种习惯。(我在F#上看到过一些非常有趣的尝试,其中函数只是一串'let'语句,基本上就像你在C程序中,用'let'替换了所有分号。:))

但另一种可能性是,您可能还没有意识到您正在简单地解决需要OOP语言中的设计模式的问题。

当您使用柯里化,或者将一个函数作为参数传递给另一个函数时,请停下来想想如何在OOP语言中做到这一点。

是否有任何事实证明函数式编程消除了需要OOP设计模式?

是的。:)当你使用FP语言时,你不再需要特定于OOP的设计模式。但你仍然需要一些通用的设计模式,比如MVC或其他非OOP特定的东西,相反,你需要几个新的特定于FP的“设计模式”。所有语言都有其缺点,设计模式通常是我们解决它们的方式。

无论如何,您可能会发现尝试“更干净”的FP语言很有趣,例如ML(我个人最喜欢的,至少出于学习目的)或haskell,当您面对新事物时,您没有OOP拐杖可以依靠。


正如预期的那样,一些人反对我将设计模式定义为“修补语言中的缺点”,所以我的理由如下:

如前所述,大多数设计模式都特定于一种编程范式,有时甚至是一种特定的语言。通常,它们解决的问题在该范式中只有存在(请参阅FP的monads,或OOP的抽象工厂)。

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

所以,如果一个问题存在于OOP语言中,而FP语言中不存在这个问题,那么很明显这就是OOP语言的缺点。这个问题是可以解决的,但你的语言并没有解决这个问题,而是需要你的一堆样板代码来解决它。理想情况下,我们希望我们的编程语言能神奇地解决0号问题。任何仍然存在的问题,原则上都是语言的缺点。;)

当你试图从“设计模式”(一般来说)和“FP与OOP”的层面来看待这个问题时,你会发现答案充其量是模糊的。

但是,在两个轴上更深一层,考虑特定设计模式特定语言功能,事情变得更加清晰。

因此,例如,一些特定的模式,如访客战略命令观察员在使用代数数据类型和模式匹配关闭第一类函数等语言时肯定会改变或消失。

总的来说,我会说,随着时间的推移,特定的模式正在被新的(或者只是越来越流行的)语言特性所淘汰。这是语言设计的自然过程;随着语言变得越来越高级,以前只能在使用示例的书中调用的抽象现在变成了特定语言特性或库的应用程序。

(旁白:这是我写的最近的博客,其中有更多关于FP和设计模式的讨论的其他链接。

Norvig的演讲提到了他们对所有GoF模式的分析,他们说23个模式中有16个在函数式语言中有更简单的实现,或者只是语言的一部分。所以大概至少有7个模式要么a)同样复杂,要么b)不存在于语言中。对我们来说不幸的是,它们没有被列举出来!

我认为很明显,GoF中的大多数“创建”或“结构”模式只是让原始类型系统在Java或C++中做你想做的事情的技巧。

一个可能是原型;虽然它是JavaScript的基本概念,但它必须从头开始在其他语言中实现。

我最喜欢的模式之一是空对象模式:将某些东西的缺失表示为一个不做任何适当事情的对象。这可能更容易用函数式语言建模。然而,真正的成就是视角的转变。

甚至OO设计模式解决方案也是特定于语言的。

设计模式是针对编程语言无法解决的常见问题的解决方案。在Java,Singleton模式解决了one-of-的(简化的)问题。

在Scala中,除了Class之外,你还有一个名为Object的顶级构造。它是懒惰实例化的,并且只有一个。

函数式编程不能取代设计模式。设计模式不能被取代。

模式只是存在;它们是随着时间的推移而出现的。GoF的书将其中的一些正式化了。如果随着开发人员使用函数式编程语言而出现新的模式,那将是令人兴奋的事情,也许也会有关于它们的书籍。

如果不提出类型系统,就无法进行此讨论。

函数式编程的主要特性包括作为一等值的函数、柯里化、不可变值等,在我看来,OO设计模式是否接近这些特性并不明显。

这是因为这些特性没有解决OOP所解决的问题……它们是命令式编程的替代品。OOP的FP答案在于ML和Haskell的类型系统……特别是和类型、抽象数据类型、ML模块和Haskell类型类。

但是当然,仍然有一些设计模式是FP语言无法解决的。FP相当于单例的是什么?(暂时忽略单例通常是一种可怕的模式)

类型类要做的第一件事是消除对单例的需求。

你可以浏览23个名单并删除更多,但我现在没有时间。

我想说的是,当您拥有像Lisp这样支持宏的语言时,您可以构建自己的特定领域抽象,这些抽象通常比一般的习语解决方案好得多。

布赖恩关于语言和模式之间紧密联系的评论非常中肯,

这个讨论缺少的部分是习语的概念。詹姆斯·O·柯普连的书《高级C++》(AdvancedC++)对这里的影响很大。早在他发现克里斯多夫·亚历山大和没有名称的列之前(如果不阅读亚历山大,你就无法明智地谈论模式),他就谈到了掌握习语在真正学习一门语言中的重要性。他用C中的字符串复制作为例子,while(*from++ = *to++);你可以把它看作是缺少语言功能(或库功能)的创可贴,但真正重要的是,它是一个比任何部分都更大的思维或表达单元。

这就是模式和语言试图做的事情,让我们能够更简洁地表达我们的意图。思想单元越丰富,你可以表达的思想就越复杂。拥有丰富的、共享的词汇——从系统架构到小摆弄——可以让我们进行更明智的对话,并思考我们应该做什么。

作为个体,我们也可以学习。这是整个练习的重点。我们每个人都可以理解和使用我们自己永远无法想到的东西。语言、框架、库、模式、习语等都在分享知识财富方面占有一席之地。

GoF书明确地将自己与OOP联系在一起——标题是设计模式——可重用面向对象软件的元素(强调我的)。

我想插入Jeremy Gibbons的几篇优秀但有点密集的论文:“Design模式作为高阶数据类型泛型程序”和“迭代器模式的本质”(都可以在这里找到:http://www.comlab.ox.ac.uk/jeremy.gibbons/publications/)。

它们都描述了惯用功能构造如何覆盖其他(面向对象)设置中特定设计模式所覆盖的领域。

我认为只有两个GoF设计模式是为了将函数式编程逻辑引入自然OO语言而设计的。我想到了战略和命令。其他一些GoF设计模式可以通过函数式编程进行修改,以简化设计并保持目的。

这是另一个链接,讨论这个话题:http://blog.ezyang.com/2010/05/design-patterns-in-haskel/

Edward在他的博客文章中用Haskell描述了所有23个原始的GoF模式。

基本上,是的

  • 当模式绕过缺失的特征(高阶函数、流处理……)时,最后通牒会促进组成
  • 需要一次又一次地重写模式的实现本身可以被视为语言气味

此外,这个页面描述提供了一个“模式/特征”翻译表和一些很好的讨论,如果你愿意挖掘的话。

模式是解决类似问题的方法,它们被一次又一次地看到,然后被描述和记录。所以不,FP不会取代模式;然而,FP可能会创建新的模式,并使当前的一些“最佳实践”模式“过时”。

GoF设计模式正在为OO语言编写变通方案,这些语言是Simula 67的后代,如Java和C++。

设计模式处理的大多数“弊病”是由以下原因引起的:

  • 静态类型的类,指定对象,但本身不是对象;
  • 限制为单次分派(只有最左边的参数用于选择方法,其余参数仅被视为静态类型:如果它们有动态类型,则由方法使用特别方法进行排序);
  • 常规函数调用和面向对象函数调用之间的区别,这意味着面向对象的函数不能作为函数参数传递,而需要常规函数,反之亦然;和
  • “基本类型”和“类类型”之间的区别。

这些设计模式中没有一个在Common Lisp对象系统中不消失,即使解决方案的结构方式与相应的设计模式基本相同。(此外,该对象系统比GoF的书早了十多年。Common Lisp在那本书首次出版的同一年成为ANSI标准。)

就函数式编程而言,模式是否适用于它取决于给定的函数式编程语言是否具有某种对象系统,以及它是否以受益于模式的对象系统为模型。这种面向对象的类型与函数式编程不能很好地混合,因为状态的突变是最前沿和最中心的。

构造和非突变访问与函数式编程兼容,因此与抽象访问或构造有关的模式可能适用:工厂、门面、代理、装饰器和访问者等模式。

另一方面,状态和策略等行为模式可能不适用于函数式OOP,因为状态的突变是它们的核心。这并不意味着它们不适用;也许它们以某种方式与模拟可变状态的任何技巧结合使用。

在2013年名为“函数式编程模式-在Scala和Clojure中”的新书中,作者Michael. B. Linn在许多情况下比较并提供了GoF模式的替代品,并讨论了较新的功能模式,如“尾递归”,“memoization”,“惰性序列”等。

这本书可以在亚马逊上找到。当我来自几十年的OO背景时,我发现它非常有用和令人鼓舞。

正如公认的答案所说,OOP和FP都有其特定的模式。

然而,有一些模式是如此常见,以至于我能想到的所有编程平台都应该有。这是一个(不完整的)列表:

  • 适配器。我很难想到一个有用的编程平台是如此全面(和自我实现),以至于它不需要与世界交谈。如果要这样做,肯定需要一个适配器。

  • 外观。任何能够处理大源代码的编程平台都应该能够模块化。如果你要为程序的其他部分创建一个模块,你会希望隐藏代码的“脏”部分,并给它一个漂亮的界面。

  • 解释器。一般来说,任何程序都只做两件事:解析输入和打印输出。鼠标输入需要解析,窗口小部件需要打印出来。因此,拥有嵌入式解释器为程序提供了额外的自定义功能。

此外,我注意到在典型的FP语言Haskell中,有一些类似于GoF模式的东西,但名称不同。在我看来,这表明它们存在,因为FP和OOP语言都有一些共同的问题需要解决。

  • 单子变形器和装饰器。前者用于为现有单子添加附加能力,后者为现有对象添加附加能力。

OOP和GoF模式处理状态。OOP对现实建模,以使代码库尽可能接近给定的现实需求。GoF设计模式是为解决原子现实世界问题而识别的模式。它们以语义的方式处理状态问题。

正如在真正的函数式编程中不存在状态一样,应用GoF模式是没有意义的。没有函数式设计模式,就像有GoF设计模式一样。每个函数式设计模式都是人为的,与现实形成对比,因为函数是数学的构造,而不是现实。

函数没有时间的概念,因为无论当前时间是多少,它们总是返回相同的值,除非时间是函数参数的一部分,这使得很难理解“未来请求”。混合语言混合了这些概念,使语言不是真正的函数式编程语言。

函数式语言的兴起只是因为一件事:当前物理学的自然限制。由于物理定律,今天的处理器处理指令的速度受到限制。你可以看到时钟频率的停滞,但处理内核的扩展。这就是为什么指令的并行性对提高现代应用程序的速度变得越来越重要。由于函数式编程的定义是没有状态的,因此没有副作用,安全并行处理函数是安全的。

GoF模式并没有过时。它们至少对于建模现实世界的需求是必要的。但是如果你使用函数式编程语言,你必须将它们转换为混合等价物。最后,如果你使用持久性,你没有机会只制作函数式程序。对于程序的混合元素,仍然有必要使用GoF模式。对于任何其他纯函数式元素,没有必要使用GoF模式,因为没有状态。

因为GoF模式对于真正的函数式编程不是必需的,这并不意味着不应该应用SOLID原则。SOLID原则超出了任何语言范式。

OOP和FP有不同的目标。OOP旨在封装软件组件的复杂性/移动部分,FP旨在最小化软件组件的复杂性和依赖性。

然而,这两种范式不一定是100%矛盾的,可以一起应用以从两个世界中获益。

即使使用像C#这样本身不支持函数式编程的语言,如果你了解FP原则,你也可以编写函数式代码。同样,如果你了解OOP原则、模式和最佳实践,你可以使用F#应用OOP原则。无论你使用哪种编程语言,你都会根据你试图解决的情况和问题做出正确的选择。

在函数式编程中,设计模式有不同的含义。事实上,大多数OOP设计模式在函数式编程中是不必要的,因为抽象级别更高,HOFs用作构建块。

HOF的原则意味着函数可以传递为其他函数的参数。和函数可以返回值。

函数式编程的最重要特征,IMHO,是您在编程时只使用表达式--表达式中的表达式中的表达式,这些表达式都计算到最后一个,最终的表达式“在计算时加热机器”。

在我看来,面向对象编程的最重要特征是,你正在使用具有内部状态的对象进行编程。在纯函数中你不能有内部状态——面向对象编程语言需要声明才能使事情发生。(函数式编程中没有语句。)

面向对象编程的模式不适用于函数式编程,因为函数式编程是使用表达式编程,而面向对象编程是使用内部状态编程。

振作起来。

听到我声称已经取代了设计模式并揭穿了SOLID和DRY,这会激怒许多人。我什么都不是。尽管如此,我正确地建模了协作(制造)架构,并在我的网站http://www.powersemantics.com/上在线发布了构建流程的规则以及背后的代码和科学。

我的论点是,设计模式试图实现制造业所谓的“大规模定制”,这是一种过程形式,其中每个步骤都可以被重塑、重组和扩展。你可能会认为这些过程是未编译的脚本。我不打算在这里重复我的(在线)论点。简而言之,我的大规模定制架构通过实现这种灵活性取代了设计模式,而没有任何混乱的语义学。我很惊讶我的模型效果如此之好,但是程序员编写代码的方式根本无法与制造业如何组织协作工作相提并论。

  • 制造=每个步骤与一个产品相互作用
  • OOP=每个步骤都与自身和其他模块交互,像无用的上班族一样将产品从一个点传递到另一个点

这种架构从不需要重构。还有一些关于集中化和分布式的规则也会影响复杂性。但是回答你的问题,函数式编程是另一组处理语义学,不是大规模自定义流程的架构,其中1)源路由作为(脚本)文档存在,使用者可以在触发前重写,2)模块可以轻松动态地添加或删除。

我们可以说OOP是“硬编码流程”范式,而设计模式是避免这种范式的方法。但这正是大规模定制的全部意义所在。设计模式将动态流程体现为凌乱的硬代码。只是没有意义。F#允许将函数作为参数传递的事实意味着函数式和OOP语言都试图完成大规模定制本身。

对读者来说,代表脚本的硬代码有多混乱?如果你认为编译器的消费者为这些功能付费,那就完全不会了,但对我来说,这些功能是语义上的浪费。它们毫无意义,因为大规模定制的目的是让进程本身成为0,而不仅仅是对使用Visual Studio的程序员来说是动态的。

有些模式在支持FP的语言中更容易实现。例如,可以使用很好的闭包来实现Strategy。然而,根据上下文,你可能更喜欢使用基于类的方法实现Strategy,比如策略本身非常复杂和/或共享你想使用模板方法建模的结构。

根据我使用多范式语言(Ruby)开发的经验,FP实现在简单的情况下运行良好,但在上下文更复杂的情况下,基于GoF OOP的方法更适合。

FP方法不会取代OOP方法,它是对OOP方法的补充。

确实如此,因为高级函数式PL(如OCaml,包含类、模块等)在类型通用性和表达能力方面肯定会取代OOP命令式语言。抽象不会泄漏,你可以直接在程序中表达你的大部分想法。因此,是的,它确实取代了设计模式,无论如何,与函数式模式相比,大多数设计模式都简单得可笑。

举一个你所陈述的错误前提的例子。

我们在OOP中作为用例适配器(例如在清洁拱门和ddd中)的适配器模式可以通过Option的monad变体在功能中实现。

你不是在取代他们,而是在改造他们。