讨论设计模式的时候到底在说什么呢

在软件工程里面,经常会看到设计模式这个词。对大部分工程师来说,尤其是Java背景的,大概从本科开始就在学习设计模式吧。四人帮,23套设计模式,更是从考卷到面试再到博客上经久不息的话题。

不过「设计模式」这个概念本身想表达什么,似乎多少有点模糊了呢。

这里有一些「官方」的定义:

  • 在软件工程中,设计模式是对软件设计中普遍存在(反复出现)的各种问题,所提出的解决方案。

    设计模式并不直接用来完成代码的编写,而是描述在各种不同情况下,要怎么解决问题的一种方案。面向对象设计模式通常以类别或物件来描述其中的关系和相互作用,但不涉及用来完成应用程序的特定类别或物件。设计模式能使不稳定依赖于相对稳定、具体依赖于相对抽象,避免会引起麻烦的紧耦合,以增强软件设计面对并适应变化的能力。 并非所有的软件模式都是设计模式,设计模式特指软件“设计”层次上的问题。还有其他非设计模式的模式,如架构模式。同时,算法不能算是一种设计模式,因为算法主要是用来解决计算上的问题,而非设计上的问题。

这是维基百科的定义,但是仔细一看,感觉就很空洞,似乎什么都说了,但似乎什么也没说。

  • 设计模式是软件设计中常见问题的典型解决方案。 每个模式就像一张蓝图, 你可以通过对其进行定制来解决代码中的特定设计问题。

这是Refactoring.guru的定义,虽然强调了设计模式的特点,但是好像还是缺了点什么。

也许可以换个角度,比如说——在JavaScript或者Go语言的使用者角度,他们会排斥设计模式,认为设计模式是「语法表达力不足的语言里用来弥补的方式」,而在语言功能较为丰富,语法较为多样的JavaScript或者Go语言里,这种模式就是多此一举甚至有时候是有害的了。

不过,真的是这样吗。

也许函数式语言可以提供另一种视角。

  • 一方面,许多Java的设计模式在函数式语言里,都不再成为什么高深的名词术语,这一点,我之前的一些日记也可以体现出来。
  • 另一方面,函数式编程语言有自己特有的设计模式。

但是,在这个话题上,社区的意见分歧也很大。

  • 例如MIT 6.031就把map/reduce/filter等高阶函数定义为一种设计模式,这种视角并没有被所有人认同
  • 更多人可能会把例如Monad这样的经典结构认定为一种设计模式,甚至「一类」设计模式,例如这里。虽说某种程度上Monad确实可以算是触及函数式编程本质的东西,不过这里不额外展开了。
  • 最后的一种例子,可能更接近OOP设计模式「扩充语言表达力」的概念,例如这个文章提到的Phantom Type、ReaderT和Trees that grow(我之后也会读一下这些讨论)。

换句话说,一些OOP的设计模式可以在FP语言里用声明式的语法,甚至可能一行就表达出来了。另外一方面,FP语言也有属于自己的设计模式。

该怎么解释这个现象呢?

首先,回到对设计模式的一般定义,「在某种特定的情况下,针对特定种类的问题,特化的解决方案」,从设计模式的三种类型也可以看出设计模式的初衷是为了更好的解决数据的处理和聚合。而在函数式编程里,这恰好是编程语言设计的核心——通过函数的组合来处理数据流。所以,哪怕只是map/fold这样简单的函数,都可以成为解决「特定问题」的答案。

另一方面,函数式编程语言也有自己的特点,例如变量不可变、无副作用,这些特殊的情况也就让程序员不得不采用更灵活的方式来应对,例如用Monad的IO来实现副作用和交互功能。

不过这么一来,也就有了另一个有趣的问题,像Actor、STM、消息队列、事件循环这些概念,算不算是FP甚至OOP的设计模式呢?设计模式的背后,有没有什么计算模型的影子呢?

先撇开这个不提。

另一个有趣的角度是DSL。在OOP里面,这个词经常会和个别设计模式,例如建造者模式结合起来。比如说一些Java的类库会提到实现了流畅接口(Fluent interface),但实际上算是一种建造者模式。与此类似的,是函数式编程里,DSL和声明式也常常被提及,尤其是在和Monad相关的情况下。

从这里大概也可以看出,DSL中的Language不一定是一种编程语言,而可以是某种意图或者模式的semantic。这种意图可以是形式化的,例如一组函数调用链,但也可以是抽象概括的,需要程序员自行编写的。从这里不难看出,DSL和设计模式之间隐约存在的相对关系(当然这里提的不一定是23种设计模式)。

其实今天想写这个,也是因为之前在想一个问题,「设计模式是否可以理解为动态扩展AST(抽象语法树)的行为」。现在看来,这个比喻大概是过于片面而忽视了一些抽象的层面的。

说到这里也想提一下这个讨论。说到底,设计模式是为了解耦和可维护可扩展性。但是有些语言的表达力贫乏到连一些很常见的东西都需要弄个复杂模型的时候,似乎就有什么东西不对劲了。而另一方面,也许对设计模式来说,理解「对于一些常见情形需要一种模式化的解决方案」,比熟背23种模式更重要。毕竟,理解了前者,那么至少可以为学习Monad或者Actor扫清第一个障碍。而只会熟背23种模式,是显然不太行的。

也想起了学校上课讲到的Active object,从异步的角度讲,这个模式几乎毫无意义,因为拆分为6个class的一百多行(Java)代码,完全可以在更优雅的语言(比如支持await的情况下)里用寥寥几行来实现,而不需要这个模式带来的学习负担和排错时的复杂调用链。不过,如果换个角度来说呢?Active object之所以说「Active」,因为对象并不是被动地被调用,而是每个对象都持有一个或多个线程,并且由这个对象来决定线程的执行。那么,如果把这里的对象换成事件循环呢?

看起来我们就得到了一个稍微宽松的actor模式,虽然学习曲线还是很陡峭,但是异步的特性在actor模式里面得到了更好的体现,线程可以被更好的复用,系统也具有更好的性能和伸缩性了。

相比之下,另一些设计模式,例如Null Object,可能就是纯粹的糟粕了,因为它们不能提供任何语言和抽象语法树上的张力,而纯粹是为了「填补某些语言贫乏的表现力」而存在的。

也许这也是一个很好的体现「设计模式到底在讲什么」的例子吧。


讨论设计模式的时候到底在说什么呢
http://inori.moe/2023/10/08/what-do-we-talk-about-design-patterns/
作者
inori
发布于
2023年10月8日
许可协议