小论工厂模式(们)

这是「从函数式角度看设计模式」的第四篇。目的是从和OOP不同的角度重新审视设计模式。

今天讨论的是工厂模式。但是其实并没有一个叫工厂模式的设计模式,一般来说,有抽象工厂和工厂方法这两种模式。

  • 抽象工厂「能创建一系列相关的对象, 而无需指定其具体类」。
  • 工厂方法「在父类中提供一个创建对象的方法, 允许子类决定实例化对象的类型」。

两种模式都属于「创建型模式」,换句话说就是和程序里面值的创建有关。

先分别用OOP代码展示一下两种模式好了。抽象工厂的简单代码例子大概可以这样写:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
// 假设我们有一些已有的按钮和「Select」(单选框或者复选框)
interface IButton {
abstract fun render()
}

interface ISelect {
abstract fun render()
}

class NativeButton : IButton {
override fun render() = ... // 使用操作系统的原生图形API
}

class WebButton : IButton {
override fun render() = ... // 使用浏览器的图形API
}

class NativeSelect : ISelect {
override fun render() = ... // 使用操作系统的原生图形API
}

class WebSelect : ISelect {
override fun render() = ... // 使用浏览器的图形API
}

// 工厂模式的实现如下:

interface IGUIFactory {
abstract fun createButton(): IButton
abstract fun createSelects(): ISelect
}

class NativeGUIConfig : IGUIFactory {
// 一个所有子元件都是原生组件的配置环境
override fun createButton(): NativeButton
override fun createSelects(): NativeSelect
}

class WebGUIConfig : IGUIFactory {
// 一个所有子元件都是浏览器渲染的配置环境
override fun createButton(): WebButton
override fun createSelects(): WebSelect
}

class Application(private val config: IGUIFactory) {
fun createUIButton(): IButton = config.createButton()
fun createUISelects(): ISelect = config.createSelects()
}

... {
...
application.createUIButton().render()
...
}

fun main(...) {
val config = when (args["platform"]) {
"Windows", "MacOS", "Linux" -> NativeGUIConfig()
"Web" -> WebGUIConfig()
else -> throw IllegalArgumentException("Invalid platform")
}

val application = Application(config)
...
}

可以看出抽象工厂给出了一种「同一套API可能返回不同种类的值的组合」的情况下的一种简单的处理方式。这里的不同种类,并不是说这些值可以被自由选择(例如在这里就不可以同时选择NativeButton和WebSelect),而更像是吃晚饭的时候有不同的菜单可以选择这样的样子吧。

工厂方法的简单代码例子大概是这样的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
interface IButton {
abstract fun onClick(...)
abstract fun render()
}

interface IDialog {
abstract fun createButton(): IButton
fun render () {
val someBtn = createButton()
someBtn.onClick(...)
someBtn.render()
}
}

class RadioButton : IButton {
override fun onClick(...) = ... // 更新一个被选中的属性
override fun render() = ... // 渲染代码
}

class CheckButton : IButton {
override fun onClick(...) = ... // 更新多个被选中的属性
override fun render() = ... // 渲染代码
}

class RadioDialog : IDialog {
override fun createButton(): IButton = RadioButton()
}

class CheckDialog : IDialog {
override fun createButton(): IButton = CheckButton()
}
```​

工厂方法相对简单一些,创建类看起来更像是一个典型的SAM(函数式接口)类型。

之前也没少提到过SAM类型,那么,按照这个提示,工厂方法可以被很简单的用lambda来改造:

```kotlin
interface IButton {
fun onClick()
fun render()
}

// 两个button类的定义一样,略过

fun renderDialog(createBtnFunc: () -> IButton) {
val someBtn = createBtnFunc()
someBtn.onClick()
someBtn.render()
}

fun main() {
renderDialog { RadioButton() } // 使用Radio按钮渲染对话框
renderDialog { CheckButton() } // 使用Check按钮渲染对话框
}

这里把IDialog和两个Dialog类型直接变成了接受lambda的函数和函数调用,用一种更简洁更灵活的方式来实现相同的机制。

在Kotlin里面,lambda有特殊的{ }语法来标识函数调用,利用块作用域让它看起来更像类或函数的声明语法。如果把这种写法扩展出去,理论上,每个UI组件都可以用简单的函数而不是类型来定义,这样的声明式写法可以在Kotlin的Compose框架中看到。也更好地体现了「函数式组合」的概念。

抽象工厂的函数式替代类似,只不过一个lambda变成了多个lambda。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// 省略之前的相同定义

data class GUIConfig(
val createBtnFunc: () -> IButton,
val createSelFunc: () -> ISelect
)

class Application(private val config: GUIConfig) {
fun createUIButton(): IButton = config.createBtnFunc()
fun createUISelect(): ISelect = config.createSelFunc()
}

... {
...
application.createUIButton().render()
...
}

fun main(...) {
val config = when (args["platform"]) {
"Windows", "MacOS", "Linux" -> GUIConfig(
createBtnFunc = { NativeButton() },
createSelFunc = { NativeSelect() }
)
"Web" -> GUIConfig(
createBtnFunc = { WebButton() },
createSelFunc = { WebSelect() }
)
else -> throw IllegalArgumentException("Invalid platform")
}

val application = Application(config)

​ 多个lambda的写法也许会比较冗长,所以在一些情况下,可能还是OOP的写法比较好写,当然在纯函数语言里就是另一回事了。

也发现有不少模式都和高阶函数有关,也许这是因为函数作为参数和返回值在函数之间传递的时候,实际上是把算法抽象成一个可以传递的值的吧。这样的过程也就体现了一种「行为或者策略的注入」,换句话说,就是依赖注入了。另一个概念是多态,当然了,函数式编程一般用的是ad-hoc或者parametric的,和OOP广泛使用的继承是很不一样的。

顺便一提,前面提到的「换菜单」也有对应的「组装单品的组合」的模式,一般来说,在OOP里面,这叫做建造者模式,也许以后有空再讨论吧。


小论工厂模式(们)
http://inori.moe/2023/06/25/factory-patterns/
作者
inori
发布于
2023年6月25日
许可协议