函数式编程中的「抽象工厂」模式

这几周没去上系分课,下载课件后发现课程已经讲解到「抽象工厂」模式了,这让我想起 Peter Norvig 有一个演讲「Design Patterns in Dynamic Languages」,其中谈到《设计模式》书中的 23 个模式,其中有 16 个在动态语言中「不见了,或者简化了」,列表中的第一个模式就是抽象工厂模式。他讨论的是 Dylan 和 Lisp,但我打算用 Racket 实现一下老师的课件上给出的例子。

对于一个工厂来说(此处「工厂」仅指代「生产者」),它的产品可能是多种多样的,如果客户需要特定的商品甚至是个性化的商品,就需要告诉工厂(葱油拌面不要葱、珍珠奶茶不要糖、可口可乐去掉冰)。也就是说,工厂需要提供接口来接收客户传入的状态,与此同时,工厂本身也带有一定的状态。在面向对象编程中,工厂由对象表示,客户的状态依靠成员函数来获取,而工厂本身的状态由数据成员和成员函数的局部变量共同决定;而在函数式编程中,一个函数本身就可以是一个工厂,客户的状态依靠参数获取,而工厂本身的状态由函数内部定义的变量决定。

进一步说,如果工厂本身的状态也需要个性化呢?这就引出了「抽象工厂」的概念。就拿课件上的例子来说,一家 Pizza 店提供芝士和蔬菜两种原料,顾客可以选择购买芝士 Pizza 或蔬菜 Pizza。但是 Pizza 原料的供应商也有两个,一个来自纽约,另一个来自芝加哥。由于一家 Pizza 店的原料只能由其中的一个供应商提供(不由顾客决定),因此可以考虑定义一个抽象工厂,让抽象工厂来决定生产哪种地方的原料,而抽象工厂可以在定义或创建具体的 Pizza 店时再提供。

课件上提供的方法是:建立一个 Pizza 店类,然后派生出纽约风味 Pizza 店和芝加哥风味 Pizza 店。而不管是芝士 Pizza 的制作还是蔬菜 Pizza 的制作,使用的都是一个抽象的原料供应商,并没有指明地域。只有在 Pizza 店类的派生类中才指明原料供应商的地域。例如,在纽约风味 Pizza 店类中,createPizza 成员函数设定原料由纽约原料供应商提供,具体化原来的抽象原料供应商,这时只需要根据用户需求制作芝士 Pizza 或者蔬菜 Pizza 就行了。因为 Java 的代码比较冗长,这里就不写出来了。

我用 Racket 实现的方法是:把一个闭包当成 Pizza 店,外面再包一层函数(可以想象成「制造工厂的工厂」)。Pizza 店(闭包)接收 item 参数以供顾客指定芝士 Pizza 还是蔬菜 Pizza,而制作 Pizza 的时候使用一个抽象的原料供应商,这个抽象的原料供应商同时也是外层函数的参数,因此可通过传递一个具体的原料供应商来获得一个具体地域风味的 Pizza 店。这里的「具体的原料供应商」同样可以用闭包表示,并编写一个「制造原料工厂的工厂」,根据地域来产出不同地域的原料供应商闭包。

注:一个闭包就是一个可以访问自身环境中一个或多个局部变量的函数。在对闭包连续的调用过程中,这些变量值将会保存在闭包中,使得闭包能记住并访问其所在的词法作用域。所以说,要创建一个新的闭包,我们还必须创建非局部变量。这也就是为什么一个闭包结构通常涉及两个函数:闭包本身和一个用于创建闭包以及封装其变量的工厂。

#lang racket

(struct pizza (name ingredient))

(define (get-ingredient-factory style)
  (define (ny-ingredient-factory item)
    (cond
      [(eq? item 'Cheese) 'NyCheese]
      [(eq? item 'Veggie) 'NyVeggie]))
  (define (chicago-ingredient-factory item)
    (cond
      [(eq? item 'Cheese) 'ChicagoCheese]
      [(eq? item 'Veggie) 'ChicagoVeggie]))
  (cond
    [(eq? style 'Ny) ny-ingredient-factory]
    [(eq? style 'Chicago) chicago-ingredient-factory]))

(define (get-pizza-store ingredient-factory)
  (define (create-pizza item)
    (cond
      [(eq? item 'cheese)
       (pizza "Cheese Pizza" (ingredient-factory 'Cheese))]
      [(eq? item 'veggie)
       (pizza "Veggie Pizza" (ingredient-factory 'Veggie))]))
  create-pizza)

;; Example:
(define i-factory (get-ingredient-factory 'Chicago))
(define p-store (get-pizza-store i-factory))
;; Get a Chicago Style Cheese Pizza
(define my-pizza1 (p-store 'cheese))
;; Get a Chicago Style Veggie Pizza
(define my-pizza2 (p-store 'veggie))

Updated: