Lambda和策略模式

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

第一篇就从lambda说起了,因为这是函数式编程里面最常见的概念。在设计模式里,这个概念一般叫做「策略模式」。

按照定义,「策略模式是一种行为设计模式, 它能让你定义一系列算法, 并将每种算法分别放入独立的类中, 以使算法的对象能够相互替换」。简而言之,就是不把算法写死,而是在运行时可以按需切换。一般来说,OOP的写法大概如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
interface IStrategy {
void execute(int param1, ...);
}

class StrategyA implements IStrategy {
....

@Override void execute(int param1, ...) { ... }
}

class StrategyB implements IStrategy {
....

@Override void execute(int param1, ...) { ... }
}

class StrategyC implements IStrategy {
....

@Override void execute(int param1, ...) { ... }
}

简而言之,「策略模式建议找出负责用许多不同方式完成特定任务的类, 然后将其中的算法抽取到一组被称为策略的独立类中」。

不过,如果换个角度讲,从函数式编程的角度,「算法可以互相替换」其实很简单?只要为每个算法设计一个函数,然后再在主函数里增加一个高阶函数参数,接受这样的算法作为入参,那就可以了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
isOdd :: Int -> Bool
isOdd = ...

canDivBy5 :: Int -> Bool
canDivBy5 = ...

isPrime :: Int -> Bool
isPrime x = ...

data FilterStrategy = Odd | Div5 | Prime

someProcess :: [Int] -> FilterStrategy -> (Int -> Int) -> [Int]
someProcess xs strat = case strat of
Odd -> filter isOdd xs
Div5 -> filter canDivBy5 xs
Prime -> filter isPrime xs

这里用一个函数式语言展示了一种经典的策略模式的使用场景。不过,就策略模式而言,如果追求的是运行时动态切换的能力,那么显然的,函数式语言提供了走的更远的可能性:

  • 在策略模式里,切换算法的可能性被耦合进了继承关系中,这体现在从策略基类派生出具体算法的这一层继承关系里,也体现在「『客户端』必须知晓策略之间的差异并且选择具体的策略」这一层实现里。
  • 在高阶函数作为参数传入的函数式写法中,并没有一个需要去派生的策略基类,算法之间的关系和算法与客户端(主函数,例如这里的someProcess)之间的关系仅仅是函数类型需要一致。这就让进一步的解耦成为了可能。

虽然这里我们也使用了FilterStrategy来枚举不同的具体算法,但实际上,这个枚举可以虚化,被一个「从关键词映射到具体算法」的字典所替代。也就是说,不需要预先写死代码,而在运行时可以自行选择算法,也就成为了可能。当然,这种解耦是否真的需要,也是因人而异,并且因为不同场景而有不同的要求的。

但不管如何,这也体现出函数式编程比起OOP的一个优点,那就是基于代数关系之间的结合,而不是依赖继承,从而有着更灵活的可能性。

实际上,这也可以理解为一种控制反转吧,就是说在一个函数里不去具体定义操作,而是把具体的实现注入进来。

有趣的是,在例如Java这样的OOP语言里,lambda本身只不过是SAM匿名类的语法糖,而SAM,或者说,「Functional Interface」的结构又和策略模式的写法是很相似的。这也许也提示了策略模式可以被lambda所取代的特点。


Lambda和策略模式
http://inori.moe/2022/09/25/lambda-and-strategy-pattern/
作者
inori
发布于
2022年9月25日
许可协议