- 文档
- 技术博客
- 教程
【免费下载链接】weekly
前端精读周刊。帮你理解最前沿、实用的技术。
本文是前端精读周刊「设计模式」系列的策略模式专篇。策略模式(Strategy Pattern)属于行为型模式,核心意图是定义一系列算法,把它们逐一封装成可互换的策略,使算法能够独立于使用它的客户端而变化。文章先用地图导航、响应式布局、排序算法三个贴近前端日常的场景建立直觉,再给出完整的 TypeScript 实现与结构拆解,并结合周刊内 DOM diff 调度、函数缓存、Tableau 分面等真实案例,说明何时该用、何时不该用策略模式。
策略模式是什么:一句话读懂意图
意图:定义一系列的算法,把它们一个个封装起来,并且使它们可以相互替换。本模式使得算法可以独立于使用它的客户而变化。
策略是个形象的表述,所谓策略就是方案。我们都知道任何事情都有多种方案,而且不同方案都能解决问题,所以这些方案可以相互替换。我们将方案从问题中抽象出来,这样就可以抛开问题,单独优化方案了——这就是策略模式的核心思想。
拆开来看,意图里有三个关键动作:
- 定义一系列算法:承认同一个问题存在多种解法,这是策略模式成立的前提;
- 把它们一个个封装起来:每种解法被封装成独立单元(ConcreteStrategy),互不干扰、各自可维护;
- 使它们可以相互替换:通过统一接口对外暴露,客户端只依赖接口,不依赖具体实现,因此「算法可以独立于使用它的客户而变化」。
三个生活化例子:先建立直觉
设计模式需要在日常工作里用起来。下面三个例子来自周刊原文,分别对应导航、布局与排序三种高频场景。
地图导航:同一输入,多种可达方案
我们去任何地方都可以选择步行、骑车、开车、公交,不同的方案都可以帮助我们到达目的地。很明显,应该将这些方案变成策略封装起来:接收的都是出发点和目的地,输出的都是路线。
这个例子完美体现了策略模式的输入输出契约:客户端(导航 App)不关心内部是调用了路况接口还是公交时刻表,它只需要「出发地 + 目的地 → 路线」这个稳定接口。换方案时,客户端代码一行都不用改。
布局方式:让报表内容与终端适配彻底解耦
比如我们做一个报表系统:在 PC 使用珊格(栅格)布局,在移动端使用流式布局。内容还是那些内容,只是布局方式会随着不同终端大小做不同的适配——那么布局的适配就是一种策略,它可以与报表内容无关。
我们可以将布局策略单独抽取出来,以后甚至可以适配电视机、投影仪等等不同尺寸的场景,而不需要对其他代码做任何改动。这就是将布局策略从代码中解耦出来的好处:内容模块稳定不变,变化的只有「如何摆放内容」这一个维度。
从周刊仓库看,这个思路在 可视化搭建/279.自动批处理与冻结.md 中同样成立——文中明确写到「只要尽量采用绝对定位的布局策略,就可以避免负面影响」,把「布局策略」当作独立于组件协议的、可整体替换的决策变量,正是策略思维的日常体现。
排序算法:调用.sort时底层到底在做什么
当我们调用.sort时,使用的是什么排序算法?可能是冒泡、快速、插入排序?其实无论何种排序算法,本质上做的事情都是一样的:把元素序列变成有序序列。我们可以事先将排序算法封装起来,针对不同特性的数组(如近乎有序、大量重复、数据量小)调用不同的排序算法。
数组数据特征 → 选择合适的排序策略 → 得到有序结果。排序库就是典型的策略容器:引擎作者可以不断往里面添加新策略(如针对小数组的插入排序、针对大数组的快速排序),而所有调用.sort的开发者都无感知。
意图解释:策略模式的本质是解耦与分工
意图:定义一系列的算法,把它们一个个封装起来,并且使它们可以相互替换。本模式使得算法可以独立于使用它的客户而变化。
算法可以理解为策略。我们制定许多解决某个场景的策略,这些策略都可以独立地解决这个场景的问题,这样下次遇到这个场景时,就可以选择任何策略来解决;而且我们还可以脱离场景,单独优化策略,只要接口不变即可。
这个意图本质上就是解耦,解耦之后才可以分工。想想一个复杂的系统:如果所有策略都耦合在业务逻辑里,那么只有懂业务的人才能小心翼翼地维护;但如果将策略与业务解耦,我们就可以独立维护这些策略,为业务带来更灵活的变化。
以排序为例的对应关系可以这样梳理:
| 策略模式角色 | 对应物 |
|---|---|
| Strategy(策略接口) | sort(compareFn)的入参约定:输入序列、输出有序序列 |
| ConcreteStrategy(具体策略) | 冒泡排序、快速排序、插入排序各自独立的实现 |
| Context / 客户端 | 调用.sort的业务代码,只认接口不认实现 |
结构图:两个核心角色
以下结构图来自周刊原文,
Strategy是策略公共接口,ConcreteStrategy是具体策略实现。
- Strategy:策略公共接口。它定义了策略之间的「公约」,所有策略都必须实现这组方法签名;
- ConcreteStrategy:具体策略,实现了上面这个接口。每个具体策略封装一种独立方案,例如步行导航、移动端布局、快速排序。
只要你的策略符合接口,就满足策略模式的条件。这句话点明了策略模式的边界:模式关心的不是策略内部的复杂度,而是「是否通过统一接口暴露、能否被互换」。多一个符合接口的新策略,只是往策略集合里加一项,客户端、Context、其它策略都不需要改动。
TypeScript 代码例子:一个可直接运行的策略容器
下面例子使用 TypeScript 编写,完整覆盖原文代码并补齐注释与System容器,使其可复制、可运行:
// 1. Strategy:策略公共接口 // 约定所有方案都必须实现 doSomething 方法 interface Strategy { doSomething: () => void } // 2. ConcreteStrategy:具体策略 1,实现接口中的 doSomething class Strategy1 implements Strategy { doSomething: () => { console.log('实现方案1') } } // 3. ConcreteStrategy:具体策略 2,同样实现接口 class Strategy2 implements Strategy { doSomething: () => { console.log('实现方案2') } } // 4. Context:策略容器(原文以 System 代指) // 构造函数接收一个策略,实际运行时委托给该策略执行 class System { constructor(private strategy: Strategy) {} run() { this.strategy.doSomething() } } // 使用:创建两个采用不同策略的系统 const systemWithStrategy1 = new System(new Strategy1()) systemWithStrategy1.run() // 实现方案1 const systemWithStrategy2 = new System(new Strategy2()) systemWithStrategy2.run() // 实现方案2用法要点:
new System(new Strategy1()):策略1实现的系统;new System(new Strategy2()):策略2实现的系统;- 运行时若想切换方案,只需替换传给
System的策略实例,System自身代码零改动。
这个例子里System就是上下文(Context):它持有策略、调用策略、对客户端屏蔽策略细节。只要传入的对象满足Strategy接口,System就能正常运转——这正是「算法独立于使用它的客户而变化」的直观落地。
结合实际场景的实战延伸
策略模式的价值需要在真实系统里验证。以下从周刊仓库中选取四个与该模式直接相关的案例,作为「何时用、如何用」的佐证。
DOM diff 的位移调度:两个引擎各自的排序策略
在 前沿技术/190.精读《DOM diff 原理详解》.md 中,两个前端框架面对同一个问题——如何以最小代价把旧列表变成新列表——采取了两种不同的位移策略:
- React 采用仅右移策略:对元素发生的位置变化,只将其移动到右边,「右边移完了,其他位置也就有序了」;
- Vue 采用最长递增子序列策略:找出不需要移动的最长子序列,其余元素再移动。
两者接收的输入(新旧列表)相同,输出(最终 DOM 顺序)相同,只是中间算法不同,且都能被独立替换与优化——这正是「针对同一问题、封装多种可互换算法」的策略模式在框架底层的体现。文章还点出了策略的另一面:选择了右移替代左移,就一定丢失了左移替代右移的效率,说明策略各有取舍,选择策略本身就是权衡。
函数缓存的缓存策略:从单条到 LRU 的多种取舍
在 前沿技术/160. 精读《函数缓存》.md 中,同样一个问题「缓存什么」就有多种策略:
- 只缓存最后一项:省内存,但重复调用非末位参数会反复计算;
- 缓存所有项:命中率高,但参数过多时内存无限制上涨,「最坏的情况就是触发浏览器限制或者页面崩溃」;
- 中间态策略:比如 LRU(least recently used)只保留最近使用的缓存,或使用 WeakMap 替代 Map 以便浏览器回收。
这些策略共享同一个接口(输入参数 → 命中缓存或重新计算),按需替换即可在「命中率」与「内存占用」之间切换平衡点,是策略模式解决非功能性需求的典型示范。
Tableau 的分面与单图表多轴:同一数据、不同呈现策略
在 前沿技术/117.精读《Tableau 探索式模型》.md 中,数据可视化对同一批数据也提供了两套呈现策略:
- 在行、列进行的多维度拆分使用分面策略;
- 在标记中对维度进行拆分则使用单图表多轴方式;
- 若条形图按新维度拆分,则采取「堆积柱状图」的策略;折线图则采取「多条线」的策略。
输入都是「数据 + 拆分维度」,输出都是图表,只是「怎么画」的策略不同。图表库据此可以让用户在不改变数据源的情况下切换呈现方式——布局类策略在 BI 场景的真实落地。
keepAlive 的布局策略:用策略规避副作用
在 可视化搭建/276.keepAlive 模式.md 中,keepAlive 模式「会在不改变任何协议、应用代码的情况下,解决跨父级移动导致的 Remount 问题」,但会引入新增 DOM 结构的问题。文中给出的对策是:「只要尽量采用绝对定位的布局策略,就可以避免负面影响」。
这里的「布局策略」就是一个可替换的决策变量:当外部条件变化时,可以整体切换到另一种布局策略,而组件协议与应用代码保持不变——与报表系统「PC 栅格 / 移动端流式」的示例异曲同工。
弊端:不要走极端,分支别滥用
不要走极端,不要每个分支都走一个策略模式,这样会导致策略类过多。当分支逻辑简单清晰好维护时,不需要使用策略模式抽象。
判断何时不该用,可以对照几个信号:
- 分支只有两三种,且逻辑一眼就能看懂(如
if (isMobile) {...} else {...}); - 策略本身不会再扩展,未来没有新增方案的可能;
- 策略之间存在大量共享状态,强行拆分会引入复杂的参数传递;
- 团队规模小、变更频率低,抽象带来的间接层成本大于收益。
策略模式的成本在于:每个策略是一个独立的类/模块,接口设计需要前期投入,且调用方多了一层间接跳转。只有这些成本能被「可替换性」与「可扩展性」的收益覆盖时,抽象才是划算的。
与模板模式的区别
在 设计模式/188.精读《设计模式 - Template Method 模版模式》.md 中,周刊用一段话界定了两者关系:
模版模式与策略模式有一定相似处,模版模式是改变算法的一部分,而策略模式是将策略完全提取出来,所以可以改变算法的全部。
- 模板模式:父类固定算法骨架(如
OpenDocument的「校验 → 读取 → 关闭」流程),子类只需重载CanOpen、ReadDocument等局部步骤——改变的是算法的一部分; - 策略模式:把整个算法作为可替换单元注入客户端——改变的是算法的全部。
一句话总结:模板模式在「骨架内填空」,策略模式在「整块换血」。前者适合结构稳定、仅局部变化的流程;后者适合解法多样、需要整体替换与扩展的场景。
总结
策略模式是很重要的抽象思维,我们首先要意识到问题有许多种解法,才能意识到策略模式的存在。当一个问题需要采取不同策略,且策略相对较复杂,且未来可能要拓展新策略时,可以考虑使用策略模式。
落到行动上,可以分三步走:
- 识别:列出同一问题的所有可行方案,判断它们是否共享「相同的输入输出契约」;
- 抽象:定义一个公共接口,把每种方案封装成实现该接口的独立策略;
- 注入:客户端通过构造参数或运行时赋值接收策略,从此与具体实现解耦。
无论是 DOM diff 的位移调度、函数缓存的命中策略,还是响应式布局的终端适配,本质上都是「把方案从问题中抽离出来、单独优化、按需替换」——这就是策略模式贯穿前端工程二十余年依然高频出现的原因。
讨论地址:精读《设计模式 - Strategy 策略模式》· Issue #304(dt-fe/weekly 周刊议题)
版权声明:自由转载-非商用-非衍生-保持署名(创意共享 3.0 许可证)
- 文档
- 技术博客
- 教程
【免费下载链接】weekly
前端精读周刊。帮你理解最前沿、实用的技术。
相关推荐
前端精读周刊:前端异步编程模式
前端精读周刊:前端异步编程模式 引言 你是否还在为回调地狱而烦恼?是否在Promise链式调用中迷失方向?是否对async/await的性能陷阱感到困惑?本文将
文档技术博客教程前端模块化设计原则:打造高内聚低耦合的现代前端架构
前端模块化设计原则:打造高内聚低耦合的现代前端架构 前端模块化设计是构建可维护、可扩展Web应用的核心原则。随着前端项目规模不断扩大,模块化已从可选优化变为必备
文档技术博客教程前端精读周刊:前端组件测试策略
前端精读周刊:前端组件测试策略 引言 在前端开发中,组件是构建用户界面的基本单元。随着前端应用的复杂性不断增加,确保组件的质量和稳定性变得至关重要。前端组件测试
文档技术博客教程
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考