文章目录
- 一、问题
- 二、解决方案
- 2.1 真实世界的类比
- 2.2 策略模式结构
- 2.3 适用场景
- 2.4 实现方式
- 2.5 优缺点
- 2.6 与其他模式的关系
- 三、示例代码
- 3.1 go
- 3.2 rust
- 3.2.1 通过 trait 实现
- 3.2.2 function + closure
策略模式是一种行为设计模式,它能定义一系列算法,把每种算法分别放入独立的类中,以是算法的对象能相互替换。
一、问题
一天, 你打算为游客们创建一款导游程序。 该程序的核心功能是提供美观的地图, 以帮助用户在任何城市中快速定位。
用户期待的程序新功能是自动路线规划: 他们希望输入地址后就能在地图上看到前往目的地的最快路线。
程序的首个版本只能规划公路路线。 驾车旅行的人们对此非常满意。 但很显然, 并非所有人都会在度假时开车。 因此你在下次更新时添加了规划步行路线的功能。 此后, 你又添加了规划公共交通路线的功能。
而这只是个开始。 不久后, 你又要为骑行者规划路线。 又过了一段时间, 你又要为游览城市中的所有景点规划路线。
如下图:导游代码将变得非常臃肿
尽管从商业角度来看, 这款应用非常成功, 但其技术部分却让你非常头疼: 每次添加新的路线规划算法后, 导游应用中主要类的体积就会增加一倍。 终于在某个时候, 你觉得自己没法继续维护这堆代码了。
无论是修复简单缺陷还是微调街道权重, 对某个算法进行任何修改都会影响整个类, 从而增加在已有正常运行代码中引入错误的风险。
此外, 团队合作将变得低效。 如果你在应用成功发布后招募了团队成员, 他们会抱怨在合并冲突的工作上花费了太多时间。 在实现新功能的过程中, 你的团队需要修改同一个巨大的类, 这样他们所编写的代码相互之间就可能会出现冲突。
二、解决方案
策略模式包括如下要素:
- 一组类的实现:首先找到一组不同的策略,分别实现为对应的策略类。
- 实现接口:然后使这些类实现相同的接口。
- 上下文类:用成员变量,存储接口(通过接口即存储了每种策略类的视线)。上下文类不执行任务,而是委托给策略对象执行。
上下文类不负责选择符合任务需要的算法—客户端会将所需的策略传递给上下文。实际上,上下文并不是很了解策略,它通过同样的接口方法和所有策略做交互(二该接口只需要暴露一个方法来触发所选策略中封装的算法即可)。
因此,上下文可独立于具体策略。这样就可以在不修改上下文代码和其他策略的情况下,添加新算法或修改已有算法了。
示例:如上图
- 有 RoadStrategy、Walking Strategy、PublicTransportStrategy 三种策略类。
- 他们都实现了 RouteStrategy interface。
- Navigator 上下文类持有 routeStrategy 成员变量。
- 当用户调用 Navigator.buildRoute() 方法时,其会调用其成员变量 routeStrategy.buildRoute() 方法。
在上图的导游应用中,每个路线规划算法,都可以被抽取到只有一个 buildRoute() 方法的独立类中,该方法接收起点和终点作为参数,并返回路线中途点的集合。
即使传递给每个路径规划类的参数一模一样, 其所创建的路线也可能完全不同。 主要导游类的主要工作是在地图上渲染一系列中途点, 不会在意如何选择算法。 该类中还有一个用于切换当前路径规划策略的方法(就像高德地图一样), 因此客户端 (例如用户界面中的按钮) 可用其他策略替换当前选择的路径规划行为。
2.1 真实世界的类比
假如你需要前往机场。 你可以选择乘坐公共汽车、 预约出租车或骑自行车。 这些就是你的出行策略。 你可以根据预算或时间等因素来选择其中一种策略。
2.2 策略模式结构
2.3 适用场景
-
当你想使用对象中各种不同的算法变体, 并希望能在运行时切换算法时, 可使用策略模式。
策略模式让你能够将对象关联至可以不同方式执行特定子任务的不同子对象, 从而以间接方式在运行时更改对象行为。 -
当你有许多仅在执行某些行为时略有不同的相似类时, 可使用策略模式。
策略模式让你能将不同行为抽取到一个独立类层次结构中, 并将原始类组合成同一个, 从而减少重复代码。 -
如果算法在上下文的逻辑中不是特别重要, 使用该模式能将类的业务逻辑与其算法实现细节隔离开来。
策略模式让你能将各种算法的代码、 内部数据和依赖关系与其他代码隔离开来。 不同客户端可通过一个简单接口执行算法, 并能在运行时进行切换。 -
当类中使用了复杂条件运算符以在同一算法的不同变体中切换时, 可使用该模式。
策略模式将所有继承自同样接口的算法抽取到独立类中, 因此不再需要条件语句。 原始对象并不实现所有算法的变体, 而是将执行工作委派给其中的一个独立算法对象。
2.4 实现方式
- 从上下文类中找出修改频率较高的算法 (也可能是用于在运行时选择某个算法变体的复杂条件运算符)。
- 声明该算法所有变体的通用策略接口。
- 将算法逐一抽取到各自的类中, 它们都必须实现策略接口。
- 在上下文类中添加一个成员变量用于保存对于策略对象的引用。 然后提供设置器以修改该成员变量。 上下文仅可通过策略接口同策略对象进行交互, 如有需要还可定义一个接口来让策略访问其数据。
- 客户端必须将上下文类与相应策略进行关联, 使上下文可以预期的方式完成其主要工作。
2.5 优缺点
2.6 与其他模式的关系
- 桥接模式、 状态模式和策略模式 (在某种程度上包括适配器模式) 模式的接口非常相似。 实际上, 它们都基于组合模式——即将工作委派给其他对象, 不过也各自解决了不同的问题。 模式并不只是以特定方式组织代码的配方, 你还可以使用它们来和其他开发者讨论模式所解决的问题。
- 命令模式和策略看上去很像, 因为两者都能通过某些行为来参数化对象。 但是, 它们的意图有非常大的不同。
- 你可以使用命令来将任何操作转换为对象。 操作的参数将成为对象的成员变量。 你可以通过转换来延迟操作的执行、 将操作放入队列、 保存历史命令或者向远程服务发送命令等。
- 另一方面, 策略通常可用于描述完成某件事的不同方式, 让你能够在同一个上下文类中切换算法。
- 装饰模式可让你更改对象的外表, 策略则让你能够改变其本质。
- 模板方法模式基于继承机制: 它允许你通过扩展子类中的部分内容来改变部分算法。 策略基于组合机制: 你可以通过对相应行为提供不同的策略来改变对象的部分行为。 模板方法在类层次上运作, 因此它是静态的。 策略在对象层次上运作, 因此允许在运行时切换行为。
- 状态可被视为策略的扩展。 两者都基于组合机制: 它们都通过将部分工作委派给 “帮手” 对象来改变其在不同情景下的行为。 策略使得这些对象相互之间完全独立, 它们不知道其他对象的存在。 但状态模式没有限制具体状态之间的依赖, 且允许它们自行改变在不同情景下的状态。
三、示例代码
3.1 go
https://refactoringguru.cn/design-patterns/strategy/go/example
package mainimport "fmt"// interface
type EvictionAlgo interface {evict(c *Cache)
}// 策略类
type Fifo struct {
}func (l *Fifo) evict(c *Cache) {fmt.Println("Evicting by fifo strategy")
}type Lru struct {
}func (l *Lru) evict(c *Cache) {fmt.Println("Evicting by lru strategy")
}type Lfu struct {
}func (l *Lfu) evict(c *Cache) {fmt.Println("Evicting by lfu strategy")
}// 上下文类
type Cache struct {storage map[string]stringevictionAlgo EvictionAlgo // 策略接口capacity intmaxCapacity int
}func initCache(e EvictionAlgo) *Cache {storage := make(map[string]string)return &Cache{storage: storage,evictionAlgo: e,capacity: 0,maxCapacity: 2,}
}// 变更策略实现
func (c *Cache) setEvictionAlgo(e EvictionAlgo) {c.evictionAlgo = e
}func (c *Cache) add(key, value string) {if c.capacity == c.maxCapacity {c.evict()}c.capacity++c.storage[key] = value // 业务逻辑是添加到 storage,并维护 capacity 不超过 maxCapacity
}func (c *Cache) get(key string) {delete(c.storage, key)
}func (c *Cache) evict() {c.evictionAlgo.evict(c)c.capacity--
}// 客户端
func main() {lfu := &Lfu{}cache := initCache(lfu)cache.add("a", "1")cache.add("b", "2")cache.add("c", "3")lru := &Lru{}cache.setEvictionAlgo(lru)cache.add("d", "4")fifo := &Fifo{}cache.setEvictionAlgo(fifo)cache.add("e", "5")
}// code result:
Evicting by LRU
Evicting by LFU
Evicting by FIFO
3.2 rust
3.2.1 通过 trait 实现
trait RouteStrategy {fn build_route(&self, from: &str, to: &str);
}struct WalkingStrategy;
impl RouteStrategy for WalkingStrategy {fn build_route(&self, from: &str, to: &str) {println!("Walking from {} to {}", from, to)}
}struct BikingStrategy;
impl RouteStrategy for BikingStrategy {fn build_route(&self, from: &str, to: &str) {println!("Biking from {} to {}", from, to)}
}struct Navigator<T: RouteStrategy> {strategy: T,
}impl<T: RouteStrategy> Navigator<T> {pub fn new(route_strategy: T) -> Self {Navigator {strategy: route_strategy,}}pub fn route(&self, from: &str, to: &str) {self.strategy.build_route(from, to)}
}fn main() {let navigator = Navigator::new(WalkingStrategy);navigator.route("Home", "Club");navigator.route("Club", "Work");let navigator = Navigator::new(BikingStrategy);navigator.route("Home", "Club");navigator.route("Club", "Work");
}
3.2.2 function + closure
https://refactoringguru.cn/design-patterns/strategy/rust/example#example-1
通过函数和闭包,可以把行为注入 object,从而避免定义 trait。
其实策略模式在 rust 很常见,如下:
fn main() {let a = [0i32, 1, 2];let iter = a.iter().filter(|x| x.is_positive());for x in iter {println!("{}", x)}
}// code result:
1
2
策略模式如下:
type RouteStrategy = fn(from: &str, to: &str);fn walking_strategy(from: &str, to: &str) {println!("Wakling from {} to {}", from, to)
}fn biking_strategy(from: &str, to: &str) {println!("biking from {} to {}", from, to)
}struct Navigator {route_strategy: RouteStrategy,
}impl Navigator {fn new(route_strategy: RouteStrategy) -> Self {Self { route_strategy }}fn route(&self, from: &str, to: &str) {(self.route_strategy)(from, to)}fn set_route_strategy(&mut self, route_strategy: RouteStrategy) {self.route_strategy = route_strategy;}
}fn main() {let mut nav = Navigator::new(walking_strategy);nav.route("A", "B");nav.route("B", "C");nav.set_route_strategy(biking_strategy);nav.route("A", "B");nav.route("B", "C");nav.set_route_strategy(|from, to| println!("fly from {} to {}", from, to));nav.route("A", "B");nav.route("B", "C");
}// code result:
Wakling from A to B
Wakling from B to C
biking from A to B
biking from B to C
fly from A to B
fly from B to C