这是「从函数式角度看设计模式」的第四篇。目的是从和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
| interface IButton { abstract fun render() }
interface ISelect { abstract fun render() }
class NativeButton : IButton { override fun render() = ... }
class WebButton : IButton { override fun render() = ... }
class NativeSelect : ISelect { override fun render() = ... }
class WebSelect : ISelect { override fun render() = ... }
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() }
fun renderDialog(createBtnFunc: () -> IButton) { val someBtn = createBtnFunc() someBtn.onClick() someBtn.render() }
fun main() { renderDialog { RadioButton() } renderDialog { CheckButton() } }
|
这里把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里面,这叫做建造者模式,也许以后有空再讨论吧。