设计模式何时成为问题而非解决方案?

57
我从未在需要使用设计模式的软件上工作过。根据保罗·格雷厄姆(Paul Graham)的Revenge of the Nerds文章,设计模式是缺乏抽象的迹象。
直接引用他的话,“例如,在OO世界中,你会听到很多关于‘模式’的内容。我想知道这些模式是否有时不是(c)情况下人类编译器正在工作的证据。当我在我的程序中看到模式时,我认为这是一个麻烦的迹象。程序的形状应该只反映它需要解决的问题。代码中的任何其他规律性都是一个迹象,至少对我来说,表明我使用的抽象不够强大--通常是我手动生成某个宏的扩展所需写入的。”
我只是想知道每个人是否认为设计模式被过度使用,并且是代码抽象不足的症状。
26个回答

41

我认为问题并非在于设计模式本身,而是开发人员可能会学习这些模式,然后过度应用它们或以极不合适的方式应用它们。

使用设计模式是有经验的程序员自然而然地学习的。您已经解决了许多次X问题,您知道哪种方法有效,您使用该方法,因为您的技能和经验告诉您它是合适的。 这就是一种模式,它是可以的。

但同样可能发生一种情况,即技能较低的程序员找到一种方法来解决问题,并试图将他们遇到的每个问题都塞到那个模子里,因为他们不知道其他方法。 这也是一种模式,它是有害的。


26
大家,请仔细阅读整段引述,最好甚至读一下这篇论文。Paul Graham 批评 C 语言等类似语言未提供足够的抽象手段,其中包括对模式的批评仅是他主要论点的一个补充或者说是一个例证。他的推理如下:
使用通用的策略来解决重复出现的问题是合乎逻辑的。在非常抽象的语言中,可以将这些策略形式化并放入库中。每当需要使用它们时,你只需 #include 它们、实例化它们、扩展它们或进行其他操作即可。相比之下,C 类语言没有提供必要的抽象手段。这就是存在“模式”这种东西的原因。模式是一种无法通过库代码表达的常见策略,因此每次应用时都必须明确地编写它。
Paul Graham 并不认为“模式”本身是有害的。它们是语言无法提供抽象手段的症状。在这方面,他几乎肯定是正确的。我们是否应该因此使用不同的语言,当然另外再讨论。
与此相反,提出问题的原始作者是错误的:模式并不是“代码中缺乏足够抽象的症状”,而是语言中缺乏足够抽象手段的症状。

22

模式实际上只是描述事物如何工作的一种方式。它是一种分类的方式。是否有一些程序过度使用它们?当然有。拥有模式的最大优势是,通过将某些东西分类为这个或那个,每个人都在同一页上(假设他们具有了解正在讨论的内容的知识水平)。当您拥有一套包含10,000行代码的系统时,需要能够快速确定某些内容的工作方式。

这是否意味着您应该始终使用模式?不是。这将导致强制将事物归类到一起而出现问题,但您也不应该回避使用它们。


非常好的模式总结。不幸的是,它在保罗·格雷厄姆的论点方面没有达到目标:如果你的语言提供了必要的手段来简单地#include <factory.h>,那么你就不必编写它。然后你的应用程序会更小,“工厂”也不会成为一个“模式”。 - edgar.holleis
1
它完全没有错。如果模式只是描述代码的一种方式,那么使用模式或其他抽象之间的区别与说“此代码使用工厂”和“此代码使用工厂模式”的区别相同,这是一个相当微不足道的区别。 - CurtainDog

22

我对设计模式的问题在于这个概念似乎存在一个核心谎言:如果你能将专家编写的代码分类,那么任何人只要识别并机械地应用这些分类就可以编写专业级代码。对于经验丰富的软件设计师相对较为罕见的经理来说,这听起来很棒。

问题在于这不是真的。仅仅使用“设计模式”,你无法编写专业水准的代码,就像仅仅使用裁剪图案就无法设计出专业级时装一样。


19

一个程序的形状应该只反映它需要解决的问题。

如果需求发生变化,而你的模块没有使用外观模式或者中介者模式进行抽象,则更换模块将变得非常困难,会发生什么情况呢?

设计模式是缺乏足够抽象的标志。

如果你已经正确地进行了所有抽象处理,那么你肯定会在某个地方使用到设计模式。

如果有一个非常“沉重”的对象不必被加载,代理模式将使用户免于永远等待。

我可以继续说下去,但我想我已经说得足够多了。设计模式是很好的工具,当使用正确时,但当它们被错误地使用时,问题就会出现,这就是为什么被误用的设计模式称为反模式的缘故吧。


1
如果你必须应用设计模式来使其更易于实际变更,那么有人可能会认为你正在尝试在错误的层次上更改错误的东西。 - MSN
12
根据我的经验,在代码中预先引入抽象层或设计模式,以便未来更容易地进行更改,通常会产生相反的效果,使代码变得臃肿。最容易更改的系统仍然几乎总是最小的那个。 - Michael Borgwardt
我从未遇到过使用门面模式抽象化模块时出现膨胀的情况。如果添加该文件会增加几个KB,则由于降低耦合度,这是值得的。 - geowa4
如果您已经适当地抽象化了所有内容,那么您里面就有一个在面向对象语言中被称为“模式”的东西。然而,如果语言提供了适当的抽象手段,您可以使用#include <factory.h>代替编写它。 - edgar.holleis
已经有许多完全可配置的方式来指定如何完成一个进程,它们被称为“编程语言”。开玩笑的话,过于沉迷于使事情易于交换可能会使您的代码难以理解和调试(也许比您想象的更难改变)。 - Casey

12

我认为保罗·格雷厄姆(Paul Graham)的这句话很重要:

[...]我正在使用的抽象不够强大——通常情况下,我是手动生成一些需要编写的宏的扩展[...]

这表明,如果您可以在您的语言中构建这些抽象(而您几乎可以使用Lisp做任何事情,这可能是他上面所说的),则您无需使用模式对其进行建模。

但并非每个人都使用Lisp,因此您只能在能够构建抽象的地方构建它们,并在无法构建抽象的地方使用模式。例如,在没有高阶函数的语言中,通常使用策略模式来对其进行建模。

这似乎是一个观察问题的方式:模式可以被视为问题的症状(例如,缺少高阶函数),也可以被视为解决问题的方法(例如,通过策略模式实现类似高阶函数的功能)。


非常精准。那些经常被引用作为每个人信条的GoF模式,实际上只适合Java程序员。其他工具需要其他解决方案。大多数将类似于模式,但具有广泛不同的优缺点,因此使用相同的名称是不合适的。 - Javier
1
我曾在一种具有高阶函数(特别是Common Lisp)的语言中使用策略模式。继承自彼此的“策略”以及包含易于访问的状态,极大地简化了我正在处理的问题。我本可以使用闭包完成所有工作,但那样会很麻烦。 事实上,我甚至不知道我已经使用了这种模式,直到我向朋友描述它并提到了这个短语。我查了一下,发现它完全匹配。我不明白我使用的技术如何反映出我所用的语言的任何信息。 - Pillsy
1
Pillsy,或许我们在谈论“策略模式”时想的是不同的事情。我想的是对于集合的排序策略或查询的匹配策略等简单的东西,并不涉及状态或继承。如果可能,使用高阶函数来完成这些操作,否则就使用策略模式。你所描述的似乎更为复杂,我可以想象在某些情况下使用对象而不是函数来定义策略是有道理的,即使高阶函数是可用的。 - Fabian Steeg
我的想法是,策略模式在某些情况下仍然有意义,即使您拥有高阶函数。在更简单的情况下,您只是弥补了这种缺乏,但在更复杂的情况下,策略可以推广到一阶函数无法做到的程度。如果您愿意,存在一个临界点,对象停止成为穷人的闭包,闭包开始成为穷人的对象。 - Pillsy

8
[sigh] 在人们开始运用自己的判断力之前,我们还需要反驳多少关于良好实践的抱怨呢?
简而言之:如果你在使用面向对象编程语言进行真正的工作,那么你很有可能已经实现了GoF设计模式中的一些来完成工作。你是否意识到了这一点取决于教育、视角和反思。否认你曾经这样做过或者说这些模式不存在,或者不是“必要的”或者“被过度使用”都是荒谬的——除非你只写比“Hello World”更简单的代码 ;-)
就在前几天,我不得不为一个单例模式实现一个访问者外观,以使适配器策略能够正常工作 :-P

5
术语“设计模式”是一个负担过重和混淆的术语。
有一种狭义的理解方式:基本上作为GoF书中列出的面向对象概念的集合的统称,例如Singleton、Facade、Strategy等。如果您在简历上将设计模式作为一项能力,则可能使用此定义。
一个项目很容易包含数十个单例对象。这种模式显然受益于被编码为可实例化的抽象,例如作为宏或抽象类。对于GoF书中的许多其他模式也是如此。
在我看来,Paul Graham的论点是:设计模式作为重复自己的标准方式,应该被编码为可实例化的抽象,并且由此推论,您应该使用具有非常灵活的定义可实例化抽象的语言。
但是,这个概念更加普遍——我认为他曲解了设计模式。Christopher Alexander发明了这个概念,以便将其应用于国家、城市、城镇和房屋的设计。游戏设计师有像战争迷雾、不对称能力等模式的语言。
在这个普遍意义上,设计模式是否都应该被编码为可实例化的抽象就不那么明显了。
如果您还没有阅读《模式语言》,请查看其范围 here。想象一下这样的东西适用于软件世界。GoF模式将位于目录底部:它们是实现细节。
您还能找到哪些其他模式?
我猜一个章节会有与游戏相关的模式。它可以包含游戏设计模式和实现技术,——我认为这不是一个明确的区别——例如游戏循环、世界实体、空间哈希、高度图景等等。这些都是设计模式。
现在,在一个游戏项目的范围内,您可能只有一个世界实体实现。任何使用任何语言的人都会意识到,将此世界实体作为基类的可实例化抽象。
但是,这个模式描述的是如何形成这个抽象:它包含一些位置概念,它是可渲染的等等。这个设计模式是否受益于被编码为宏或抽象类本身并不明显!这个模式已经描述了一个抽象!您会为世界实体实现创建元类吗?使用各种难以置信的参数定义世界实体类的宏?
在想象中的目录中继续向上查看,我们看到这些模式是次要的,比较通用的模式,即游戏模式。这是否值得编码为宏或抽象类?也许。但不明显。我们谈论的是一个游戏框架,在其中通过传递参数、填写空白等方式定义计算机游戏。这可能很有成效和有趣,但这并不是唯一的方法。
Erlang平台具有可实例化的抽象模式,涵盖各种通用类型,例如服务器,这很酷。我猜Erlang项目中的服务器数量与Java项目中单例的数量相同。
但是对于特定目的,如果您使用Lisp、Haskell或其他语言编写服务器,有时只需遵循服务器模式,在函数和对象中实现它,而不必尝试将其作为整个实例化抽象。
所有设计模式并非在源代码中呈现为低层文本模式。

5
我认为保罗·格雷厄姆在这方面完全错了。使用模式并不是拿起一本列出模式的书,并将其中一些应用于你的代码。我同意那样做是一个糟糕的选择,但这不是模式的意义所在。
设计模式只是一种识别“嘿,我认识到这与我以前解决过的另一个问题相似”,并学会创建一个良好的解决方案抽象,以便在适当的情况下应用它的方法。
实际上,如果你编写过任何规模合理的程序,很可能已经使用了常见的设计模式,只是不知道它有一个名称。例如,在我知道“工厂”之前,我很长一段时间都在使用“工厂”模式。了解它的正式名称只是防止我再次重复造轮子而已。
当然,一些著名的模式可能有点烂;单例模式肯定是可疑的。但这是一个明显的过度聪明的解决方案寻找要解决的问题,而且做得不太好。但这就是为什么你可以找到很多支持和反对它的文献,因为人们认识到它可能不是一个好的解决方案。
但总体来说,模式是一件非常好的事情,如果有什么,它们会鼓励抽象化。

如果语言足够抽象,你可以使用#include <factory.h>来代替手动编写。 - edgar.holleis
2
这听起来像是一个库的问题,而不是语言的问题。并且并不真正影响模式的有用性。基本上你是在说语言应该为你提供更多的模式。当然,为什么不呢。但它们仍然是模式。 - Evan Teran
1
并非每种情况都有相应的库可用。有时我们必须编写代码。拥有一个模板可以帮助我们做好准备,它可以帮助我们覆盖所有基础知识,并且可以帮助后来者更快地理解代码。 - Tony Ennis
@Tony:当然我理解,但是Edgar说的是“如果语言……”,这指向了错误的事情。如果你想要更多的模式默认可用,那么应该将其添加到标准库中,而不是语言本身。 - Evan Teran

4
我相信Paul Graham的说法是,设计模式应该用语言来表达。如果有一种X模式,意味着人们被迫重写一系列代码来执行它,那么就应该有一种特定于语言的表达方式。这可以内置到语言中(虽然很尴尬,可能会创建新的设计模式),也可以在语言内自动化实现(例如Common Lisp宏; C ++模板,特别是在使用奇怪方式时,有时可以做到相似的效果)。
例如,让我们考虑一个更简单的模式:递增循环。必须对序列中的每个元素执行某些操作非常常见,如果没有与for语句对应的内容,我们肯定会采用一些通用的编程约定来表示它,并且某些人(或某些小组)将其与其他类似的基本构造打包起来并写一本书。
同样,如果使用工厂模式,可以将工厂功能包含在语言中,或者可以在语言内部自动化实现它。具体而言,Paul希望能够编写Lisp宏来实现它,而不是一遍又一遍地编写工厂。
使用设计模式的危险在于,它是我们不断输入的熟悉代码框架,因此我们有一些代码块,我们往往不会完全阅读和编写。不便之处在于,它们是我们必须一遍又一遍地输入的代码,并且感觉应该自动化重写。
关于是否像GoF所说的那样,模式是使用语言的基本部分,还是可以像PG所说的那样抽象出来,这基本上是一个经验性问题,最好通过寻找模式本身不必要的语言并观察以查看它们是否真正需要新的模式来解决。

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