欢迎光临散文网 会员登陆 & 注册

函数式编程和设计模式(序章)

2022-07-14 00:21 作者:ZeromaX訸  | 我要投稿


前段时间想起《Java 8 实战》一书中提到,策略模式、模板模式之类的传统设计模式在使用 lambda 进行实现时可以很轻松的实现,就想针对性的查一下 GoF 的那些设计模式在函数式编程的情况下还有什么更好的实现。因此,找到了一本好书——《Scala 和 Clojure 函数式编程模式》——里面介绍了传统面向对象的一些设计模式在 Scala 和 Clojure 语言中的实现,此外还介绍了一下函数式编程独特的一些设计模式。这开拓了我自己的视野,所以打算整理整理一下自己阅读过程的一些感想,写成文章给大家分享。

在本篇序章中,我先简单回顾一下《Java 8 实战》中提到的几种在 Java 语言中的可以使用 Lambda 优化实现的设计模式。这里我们不会对具体的设计模式过多着墨,直接把相关的普通实现方式和 Lambda 实现方式的对比贴出来。预计在后续文章再分享一下结合我自己以前对设计模式的学习,现在又看了《Scala 和 Clojure 函数式编程模式》一书中的其他启发深思的设计模式实现的读后感。

下面会介绍的主要包含五个设计模式:

  • 策略模式

  • 模板模式

  • 观察者模式

  • 责任链模式

  • 工厂模式

1 策略模式

策略模式简单来说代表了解决一类算法的通用解决方案,你可以在运行时选择使用哪种方案。

策略模式包含三部分内容:

  • 一个代表某个算法的接口(它是策略模式的接口)

  • 一个或多个该接口的具体实现,它们代表了算法的多种实现。

  • 一个或多个使用策略对象的客户。

例如下面这个例子,我们希望验证输入的内容是否符合标准(不同的策略,比如只包含小写字母或数字)。我们应用设计模式可以有如下代码实现:

如果我们不声明新的类实现不同的策略,而是直接传递 Lambda 表达式来实现,代码相当简洁。如下所示:

Lambda 表达式实际上已经对部分代码(或者说策略)进行了封装,而这就是创建策略设计模式的初衷。

因此《Java 8 实战》一书中强烈建议对类似问题尽量使用 Lambda 表达式来解决。

2 模板模式

如果你需要采用某个算法的框架,同时又希望有一定的灵活度,能对它的某些部分进行改进,那么采用模板方法设计模式是比较通用的方案。换句话说,模板方法模式在你“希望使用这个算法,但是需要对其中的某些行进行改进,才能达到希望的效果”时是非常有用的。

例如对于一个可以自定义让用户满意的操作的在线银行应用,使用抽象类实现的代码如下:

大致逻辑就是先按 id 获取到用户信息,然后对应执行让用户满意的操作(makeCustomerHappy 抽象方法)。

使用 Lambda 表达式同样可以解决这些问题(创建算法框架,让具体的实现插入某些部分)。你想要插入的不同算法组件可以通过 Lambda 表达式或者方法引用的方式实现。代码如下:

那么通过第二个参数 Consumer<Customer> makeCustomerHappy,你就可以直接将原来抽象方法的逻辑作为 Lambda 表达式传入,从而插入不同的行为。从而免去了继承 OnlineBanking 抽象类的麻烦。使用过程如下面代码:


3 观察者模式

观察者模式是一种比较常见的方案,某些事件发生时(比如状态转变),如果一个对象(通常我们称之为主题)需要自动地通知其他多个对象(称为观察者),就会采用该方案。创建图形用户界面(GUI)程序时,你经常会使用该设计模式。这种情况下,你会在图形用户界面组件(比如按钮)上注册一系列的观察者。如果点击按钮,观察者就会收到通知,并随即执行某个特定的行为。

接下来所举的例子是多个新闻媒体监听某一主题的消息的情况。代码如下:

顺便翻译了一下原书代码里面玩的梗,哈哈…… 卫报(Guardian)会关注这一则新闻。

这里其实 Lambda 表达式可以做的优化就不是特别多,只是通过将观察者们提取成 Lambda 表达式表示需要执行的行为。例如卫报的逻辑我们可以这样实现:

但观察者逻辑可能十分复杂,可能还持有状态,抑或定义了多个方法。诸如此类情况时,你应该继续使用类的方式。

4 责任链模式

责任链模式是一种创建处理对象序列(比如操作序列)的通用方案。一个处理对象可能需要在完成一些工作之后,将结果传递给另一个对象,这个对象接着做一些工作,再转交给下一个处理对象,以此类推。

通常,这种模式是通过定义一个代表处理对象的抽象类来实现的,在抽象类中会定义一个字段来记录后续对象。一旦对象完成它的工作,处理对象就会将它的工作转交给它的后继。 代码中,这段逻辑看起来是下面这样:

可能你已经注意到,这就是第二节介绍的模板方法设计模式。 handle 方法提供了如何进行工作处理的框架。不同的处理对象可以通过继承 ProcessingObject 类,提供 handleWork 方法来进行创建。

使用的代码如下:

这个模式看起来就像是在链接多个函数而构造一个新的总函数。那么,我们可以使用 UnaryOperator<String>andThen 方法进行构造:


5 工厂模式

使用工厂模式,你无需向客户暴露实例化的逻辑就能完成对象的创建。比如,我们假定你为一家银行工作,他们需要一种方式创建不同的金融产品:贷款、期权、股票,等等。

通常,你会创建一个工厂类,它包含一个负责实现不同对象的方法,如下所示:

那么,因为方法引用也可以引用构造函数,所以我们可以通过这种方式来创建一个 Map,将产品名映射到对应的构造函数:

上面使用构造方法引用达到了传统工厂模式同样的效果。但是,如果工厂方法 createProduct 需要接收多个传递给产品构造方法的参数,这种方式的扩展性不是很好。你不得不提供不同的函数接口,无法采用之前统一使用一个简单接口的方式。比如,我们假设你希望保存具有三个参数(两个参数为 Integer 类型,一个参数为 String 类型)的构造函数;为了完成这个任务,你需要创建一个特殊的函数接口 TriFunction。最终的结果是 Map 变得更加复杂。


6 总结

从《Java 8 实战》一书中的 “8.2 使用 Lambda 重构面向对象的设计模式”中我们可以看到,很多的设计模式可以使用 Java 8 中引入的 Lambda 表达式和方法引用更加简洁的实现。而对于其他设计模式,更甚至在函数式编程被更丰富地支持的 Scala 语言中,会有更多的简洁实现,大大简化了我们对于设计模式的识记。某种程度上,也是函数式编程更高的抽象能力(可以对函数本身进行操作),赋予了我们更强的实现能力。

正如同迭代器模式、原型模式这些在 Java 中因为 Iterator、Cloneable 而实现大大简化。在函数式编程特性已经开始逐步普及到各个传统面向对象语言的今天,我们也许可以更多地“忘记” GoF 的 23 种设计模式,从而走上一条大道至简道路。从把书读厚(不会设计模式到会设计模式),到把书读薄(再学会利用函数式编程的方法无招胜有招),这也是个有趣的过程。

后续我会继续更新《Scala 和 Clojure 函数式编程模式》一书相关以及其他的函数式编程与设计模式的话题,并尽量尝试用 Java 复刻一下。感兴趣也可以继续关注哟~

题外话

在知乎搜索相关话题时,发现有一个整理编程界(或者说软件开发业界)类似“设计模式”的关注点的列表(已经是 2015 年的老回答了:https://www.zhihu.com/question/30190384/answer/47294641):

  1. GoF Patterns 设计模式

  2. IoC / Container, Application Server (e.g. EJB) 控制反转 / 容器、应用服务器

  3. UML / MDA 统一建模语言 / 模型驱动架构

  4. ORM  对象关系映射

  5. AOP / Meta-Programming 面向切面编程 / 元编程

  6. SOA 面向服务架构

  7. Concurrency Facility: Fiber, Green-thread / Message Passing Patterns 并发

  8. Async Facility 异步

  9. NoSQL 非关系型数据库

  10. Functional Programming / Map-Reduce 函数式编程

不得不说深有感触,很多设计模式也是相应问题下积累下来的成熟解决方案。随着时代的进步,旧问题也会有新的解答和应用,新的领域也会出现。函数式编程其实某种程度上也是经历了老树发新芽的过程,如今在以前面向对象的语言上也逐步出现了函数式编程的新特性。不得不说学无止境啊……


函数式编程和设计模式(序章)的评论 (共 条)

分享到微博请遵守国家法律