☰
前端精读周刊:Strategy 策略模式 —— 把「方案」从「问题」中解耦出来的行为型设计模式
2026/10/3 7:29:48 网站建设 项目流程
  • 文档
  • 技术博客
  • 教程

【免费下载链接】weekly

前端精读周刊。帮你理解最前沿、实用的技术。

项目地址:https://gitcode.com/GitHub_Trending/we/weekly
点击查看免费下载

本文是前端精读周刊「设计模式」系列的策略模式专篇。策略模式(Strategy Pattern)属于行为型模式,核心意图是定义一系列算法,把它们逐一封装成可互换的策略,使算法能够独立于使用它的客户端而变化。文章先用地图导航、响应式布局、排序算法三个贴近前端日常的场景建立直觉,再给出完整的 TypeScript 实现与结构拆解,并结合周刊内 DOM diff 调度、函数缓存、Tableau 分面等真实案例,说明何时该用、何时不该用策略模式。

策略模式是什么:一句话读懂意图

意图:定义一系列的算法,把它们一个个封装起来,并且使它们可以相互替换。本模式使得算法可以独立于使用它的客户而变化。

策略是个形象的表述,所谓策略就是方案。我们都知道任何事情都有多种方案,而且不同方案都能解决问题,所以这些方案可以相互替换。我们将方案从问题中抽象出来,这样就可以抛开问题,单独优化方案了——这就是策略模式的核心思想。

拆开来看,意图里有三个关键动作:

  1. 定义一系列算法:承认同一个问题存在多种解法,这是策略模式成立的前提;
  2. 把它们一个个封装起来:每种解法被封装成独立单元(ConcreteStrategy),互不干扰、各自可维护;
  3. 使它们可以相互替换:通过统一接口对外暴露,客户端只依赖接口,不依赖具体实现,因此「算法可以独立于使用它的客户而变化」。

三个生活化例子:先建立直觉

设计模式需要在日常工作里用起来。下面三个例子来自周刊原文,分别对应导航、布局与排序三种高频场景。

地图导航:同一输入,多种可达方案

我们去任何地方都可以选择步行、骑车、开车、公交,不同的方案都可以帮助我们到达目的地。很明显,应该将这些方案变成策略封装起来:接收的都是出发点和目的地,输出的都是路线。

这个例子完美体现了策略模式的输入输出契约:客户端(导航 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等局部步骤——改变的是算法的一部分;
  • 策略模式:把整个算法作为可替换单元注入客户端——改变的是算法的全部。

一句话总结:模板模式在「骨架内填空」,策略模式在「整块换血」。前者适合结构稳定、仅局部变化的流程;后者适合解法多样、需要整体替换与扩展的场景。

总结

策略模式是很重要的抽象思维,我们首先要意识到问题有许多种解法,才能意识到策略模式的存在。当一个问题需要采取不同策略,且策略相对较复杂,且未来可能要拓展新策略时,可以考虑使用策略模式。

落到行动上,可以分三步走:

  1. 识别:列出同一问题的所有可行方案,判断它们是否共享「相同的输入输出契约」;
  2. 抽象:定义一个公共接口,把每种方案封装成实现该接口的独立策略;
  3. 注入:客户端通过构造参数或运行时赋值接收策略,从此与具体实现解耦。

无论是 DOM diff 的位移调度、函数缓存的命中策略,还是响应式布局的终端适配,本质上都是「把方案从问题中抽离出来、单独优化、按需替换」——这就是策略模式贯穿前端工程二十余年依然高频出现的原因。


讨论地址:精读《设计模式 - Strategy 策略模式》· Issue #304(dt-fe/weekly 周刊议题)

版权声明:自由转载-非商用-非衍生-保持署名(创意共享 3.0 许可证)

  • 文档
  • 技术博客
  • 教程

【免费下载链接】weekly

前端精读周刊。帮你理解最前沿、实用的技术。

项目地址:https://gitcode.com/GitHub_Trending/we/weekly
点击查看免费下载

相关推荐

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询