1. 从"context-mode"这个词说起:它到底指什么
第一次看到"context-mode"这个标题,很多人会下意识地把它当成某个具体框架的配置项,或者某个库里的一个枚举值。但真正在工程里摸爬滚打过几年之后你会发现,这个词其实指向的是一类非常普遍、却又极容易被忽视的设计问题——同一套逻辑,在不同上下文语境下,应该表现出不同的行为模式。
举个最直白的例子。你写了一个数据校验函数,在用户注册表单里,它需要严格到"邮箱格式错一个字符都不行";但在后台批量导入场景里,它又需要宽容到"能修就修,修不了再报错"。这两个场景用的是同一套校验逻辑,但"上下文模式"完全不同。如果你把这两种模式硬塞进一个函数里,用一堆 if-else 去判断当前是哪个场景,代码很快就会变成一团乱麻。
这就是 context-mode 要解决的核心问题:把"当前处于什么上下文"这件事,从散落各处的条件判断,提升为一个显式的、可切换的、有明确边界的模式概念。
我在实际项目里见过太多这样的代码:一个函数签名里塞了七八个布尔参数,strict、lenient、dryRun、silent、force……调用方每次都要想半天该传哪些组合。这种设计的问题不在于功能不对,而在于它把"上下文"这件事隐式化了。读代码的人必须在大脑里模拟所有参数组合,才能理解某一次调用到底会走哪条路径。
context-mode 的思路则完全不同。它要求你先把所有可能的上下文场景枚举出来,给每个场景起一个有意义的名字,然后让代码在进入某个场景时,明确地"切换模式"。这样做的好处是:读代码的人只需要看模式名,就知道当前这段逻辑处于什么语境下,不需要再去追踪一堆布尔值的组合。
从更宏观的视角看,context-mode 这个概念在多个领域都有对应的实践。前端领域里,React 的 Context 机制本质上就是在解决"跨层级传递上下文"的问题;后端领域里,多租户系统需要根据请求来源切换数据隔离模式;甚至在 CI/CD 流水线里,同一个构建脚本在开发分支和发布分支上也需要表现出不同的行为模式。这些场景背后都是同一个问题:上下文不同,行为就该不同,而且这种不同应该是显式的、可管理的。
所以这篇内容,我想从实际工程的角度,把 context-mode 这个看似抽象的概念拆开来讲清楚。不管你是刚入行的开发者,还是已经带过几个项目的老手,只要你在工作中遇到过"同一套代码要适配多种场景"的困扰,这篇内容应该都能给你一些可以直接落地的思路。
2. 为什么隐式的上下文判断迟早会失控
2.1 布尔参数组合爆炸的真实代价
先来看一个我亲身经历过的案例。几年前我参与维护一个内部的数据导出服务,最初的需求很简单:把数据库里的记录导出成 CSV 文件。函数签名大概是这样:
def export_data(query, output_path, include_header=True): ...后来需求慢慢变多。有人要求支持导出时过滤敏感字段,于是加了一个mask_sensitive参数;有人要求支持增量导出,于是加了incremental;有人要求导出前先做数据校验,于是加了validate_first;再后来又要支持导出到不同格式,加了format参数……
半年之后,这个函数的签名变成了这样:
def export_data(query, output_path, include_header=True, mask_sensitive=False, incremental=False, validate_first=True, format='csv', encoding='utf-8', dry_run=False, batch_size=1000, on_error='skip'): ...函数体内部呢?密密麻麻的条件分支。if mask_sensitive and not dry_run:、if incremental and format == 'csv':、if validate_first or on_error == 'strict':……每次改一个需求,都要小心翼翼地检查会不会影响到其他参数组合下的行为。
这就是典型的隐式上下文问题。每个布尔参数单独看都很合理,但它们的组合构成了一个巨大的状态空间。更糟糕的是,这些组合里有很多是"非法"的——比如dry_run=True和incremental=True同时出现时到底该是什么行为?没人说得清,因为代码里根本没有显式地处理这种组合。
我后来做了一次重构,把这个函数拆成了三个明确的模式:
| 模式名 | 适用场景 | 核心行为 |
|---|---|---|
PreviewMode | 用户预览导出结果 | 只读,不写文件,不修改任何状态 |
StandardExport | 常规导出 | 写文件,跳过错误记录,记录日志 |
StrictExport | 合规审计导出 | 写文件,遇到任何异常立即中止,完整记录审计日志 |
重构之后,调用方只需要传一个模式对象,函数内部根据模式类型走不同的分支。代码量没有减少太多,但可读性和可维护性提升了一个档次。因为每个模式的行为边界是明确的,新增需求时只需要考虑"这个需求属于哪个模式"或者"是否需要新增一个模式",而不是"这个新参数会和现有参数产生什么交互"。
2.2 上下文泄漏:一个更隐蔽的坑
比参数组合爆炸更隐蔽的问题,是上下文泄漏。什么意思呢?就是某个上下文的信息,意外地影响到了另一个上下文的逻辑。
我见过一个典型的例子:一个 Web 应用同时支持"普通用户请求"和"管理员后台请求"两种上下文。开发者在处理普通用户请求时,为了调试方便,加了一段逻辑:如果请求头里带了X-Debug标记,就跳过权限校验。这段逻辑本身没问题,但它被放在了一个公共的中间件里,结果管理员后台的请求也会走这段逻辑。虽然管理员本来就有权限,但这段"跳过校验"的代码在管理员上下文里执行时,会绕过一些额外的审计日志记录,导致审计链断裂。
这就是上下文泄漏:为 A 上下文写的逻辑,意外地在 B 上下文里生效了。这类问题在代码审查时很难发现,因为单独看每一段代码都是"正确"的,只有理解了上下文的边界,才能看出问题。
context-mode 的思路对解决这类问题特别有效。当你把每个上下文都定义成一个显式的模式,并且要求所有逻辑都必须声明"我属于哪个模式"时,上下文泄漏就很难发生了。因为一个模式里的逻辑,默认不会在另一个模式里执行,除非你显式地做了模式切换。
2.3 从"到处判断"到"一处定义"的思维转变
隐式上下文判断的根源,在于我们把"当前是什么场景"这个信息,分散在了代码的各个角落。每个函数、每个分支都在自己判断"现在是不是严格模式""现在是不是预览模式"。
context-mode 要求我们做一个思维转变:把"当前是什么场景"这个信息,收敛到一个地方定义,然后让所有需要它的代码都从这个地方获取。
这个"一个地方"可以是一个模式枚举、一个配置对象、一个上下文容器,具体形式取决于你的技术栈。关键不在于形式,而在于单一事实来源原则:关于"当前处于什么上下文"这个事实,整个系统里应该只有一个地方在维护它。
这样做的好处是显而易见的。当你想知道"系统当前支持哪些上下文"时,只需要看那一个地方;当你想新增一个上下文时,也只需要改那一个地方;当你想知道"某个上下文下会发生什么"时,顺着模式定义往下追就行了,不需要在代码库里到处搜索散落的判断逻辑。
3. 设计一个 context-mode 体系的四个关键决策
3.1 模式粒度:粗一点还是细一点
设计 context-mode 体系时,第一个要做的决策就是:模式应该划分得多细?
划分得太粗,比如只有"生产模式"和"测试模式"两种,那很多场景就没法覆盖。比如同样是生产模式,"用户主动触发的操作"和"定时任务触发的操作"在错误处理上可能需要不同的策略。
划分得太细,比如每个 API 端点都定义一个模式,那模式的数量会爆炸,维护成本反而更高。而且很多模式之间的差异很小,单独定义反而增加了理解负担。
我的经验是:按"行为差异"来划分,而不是按"业务场景"来划分。具体来说,如果两个场景在错误处理、日志记录、权限校验、数据可见性这四个维度上的行为基本一致,那它们就可以归为同一个模式。反之,如果某个场景在这四个维度中有任何一个表现出明显不同的行为,那就值得单独定义一个模式。
举个例子。在一个内容管理系统中,"编辑草稿"和"编辑已发布内容"这两个场景,在业务上看起来不同,但如果它们在错误处理(都是即时反馈)、日志记录(都记录操作日志)、权限校验(都要求编辑权限)、数据可见性(都只能看到自己的内容)这四个维度上行为一致,那它们就可以共用同一个"编辑模式"。真正需要单独定义模式的,可能是"编辑模式"和"审核模式"——因为审核模式下,数据可见性不同(审核员能看到所有待审内容),错误处理也不同(审核操作通常需要更严格的确认)。
3.2 模式切换的时机与方式
第二个关键决策是:模式在什么时候、以什么方式切换?
常见的切换时机有三种:
- 请求级切换:每个请求进来时,根据请求特征(如 URL 路径、请求头、用户角色)确定模式,整个请求处理过程中模式不变。这是最常见也最容易管理的做法。
- 会话级切换:模式在用户会话建立时确定,整个会话期间保持不变。适合那些模式与用户身份强绑定的场景。
- 操作级切换:每次执行具体操作时动态确定模式。这种方式最灵活,但也最容易出错,因为模式可能在一次业务流程中多次变化。
我个人的建议是:优先选择请求级切换,只有在确实需要时才考虑更细粒度的切换。请求级切换的好处是模式的生命周期清晰,容易追踪和调试。操作级切换虽然灵活,但一旦模式在流程中间发生变化,排查问题时会非常痛苦——你很难复现"当时到底是什么模式"。
至于切换方式,我强烈建议避免在业务代码里直接修改模式。模式应该由框架层或入口层统一确定,然后以只读的方式传递给业务代码。业务代码只能读取当前模式,不能修改它。这样做可以防止模式在业务流程中被意外改变,也能让模式的确定逻辑集中在一处,便于审查和测试。
3.3 模式与配置的关系:别把两者混为一谈
很多人会把"模式"和"配置"混在一起,觉得模式不就是一组配置的集合吗?这个理解有一定道理,但不完全准确。
配置是参数,模式是语义。配置回答的是"这个值是多少",模式回答的是"我们现在处于什么语境"。两者的关键区别在于:配置可以任意组合,但模式是互斥的、有明确边界的。
举个例子。timeout=30是一个配置,retry_count=3也是一个配置。你可以同时设置这两个配置,它们之间没有冲突。但"严格模式"和"宽松模式"是两个模式,你不能同时处于这两个模式中——它们是对立的。
在实际设计中,我通常会把模式作为配置的"上层概念"。也就是说,先确定当前是什么模式,然后根据模式加载对应的配置。比如:
class ContextMode: STRICT = "strict" LENIENT = "lenient" PREVIEW = "preview" MODE_CONFIGS = { ContextMode.STRICT: { "on_error": "abort", "log_level": "debug", "validate_input": True, "audit_trail": True, }, ContextMode.LENIENT: { "on_error": "skip", "log_level": "warn", "validate_input": False, "audit_trail": False, }, ContextMode.PREVIEW: { "on_error": "skip", "log_level": "info", "validate_input": True, "audit_trail": False, "dry_run": True, }, }这样做的好处是,配置的差异被集中管理了。当你想知道"严格模式和宽松模式到底有哪些不同"时,只需要对比这两个配置字典就行了,不需要去代码里到处找。
3.4 模式的默认值与降级策略
最后一个关键决策是:当模式无法确定时,应该怎么办?
这个问题在实际系统中非常常见。比如一个请求进来,但无法从请求特征中判断它属于哪个模式,这时候系统应该怎么处理?
我的建议是:永远不要有"无模式"的状态,必须有一个明确的默认模式,而且默认模式应该是保守的。
什么叫保守?就是在不确定的情况下,选择最安全、最不容易造成损害的行为。比如默认模式应该是"只读"而不是"可写",应该是"严格校验"而不是"跳过校验",应该是"记录详细日志"而不是"静默处理"。
这个原则看起来简单,但在实际系统中经常被违反。我见过太多系统,默认模式是"宽松模式",结果一个配置错误就导致大量脏数据被写入。如果默认模式是"严格模式",那最坏的情况也只是操作被拒绝,不会造成数据损坏。
另外,降级策略也很重要。当系统检测到当前模式下的某个操作可能有问题时,应该能够自动降级到更保守的模式,而不是硬着头皮继续执行。比如一个批量操作在严格模式下失败了,系统可以自动降级到"逐条处理"的模式,而不是整个批次全部失败。
4. 在真实项目里落地 context-mode 的完整路径
4.1 第一步:盘点现有代码中的隐式上下文
落地 context-mode 的第一步,不是急着写代码,而是先盘点现有代码里有哪些隐式的上下文判断。
具体怎么做?我通常会用"关键词搜索 + 人工审查"的方式。先搜索代码库里所有可能暗示上下文判断的关键词,比如if.*mode、if.*strict、if.*debug、if.*test、if.*prod、is_admin、is_preview等等。然后把搜索结果分类,看看哪些是真正的上下文判断,哪些只是普通的业务逻辑分支。
这个盘点过程通常会揭示出一些让人惊讶的事实。比如你可能以为系统只有两三种上下文,但盘点之后发现实际上有十几种不同的隐式上下文,而且它们之间的边界非常模糊。我做过的一个项目,盘点之前团队以为只有"开发/测试/生产"三种环境上下文,盘点之后发现还有"内部用户/外部用户""同步调用/异步调用""首次操作/重复操作"等至少七八种隐式上下文,每种都有自己的一套判断逻辑。
盘点的产出应该是一份清单,列出所有发现的隐式上下文,以及它们当前是如何被判断的、影响了哪些行为。这份清单是后续设计模式体系的基础。
4.2 第二步:合并同类项,定义模式边界
有了清单之后,下一步是合并同类项。把那些在行为上基本一致的上下文合并成一个模式,把行为差异明显的拆分成不同模式。
合并的判断标准,还是前面提到的四个维度:错误处理、日志记录、权限校验、数据可见性。如果两个上下文在这四个维度上的行为一致,就可以合并;如果有任何一个维度不同,就需要考虑拆分。
这里有一个实用的技巧:先按最粗的粒度合并,然后在实际使用中逐步细化。不要一开始就追求完美的模式划分,因为你对模式的理解会随着落地过程不断加深。先定义三五个核心模式,把最明显的上下文差异管理起来,然后在后续开发中根据实际需要逐步调整。
我在一个项目中就是这么做的。最初只定义了三个模式:Interactive(用户交互)、Batch(批量处理)、System(系统内部调用)。用了两个月之后,发现Batch模式下其实有两种差异很大的子场景:一种是"用户主动触发的批量操作",需要详细的进度反馈;另一种是"定时任务触发的批量操作",只需要记录最终结果。于是把Batch拆成了InteractiveBatch和ScheduledBatch两个模式。这种渐进式的细化,比一开始就设计一个复杂的模式体系要务实得多。
4.3 第三步:建立模式传递机制
模式定义好了,接下来要解决的是模式如何在系统中传递的问题。
在大多数系统里,模式需要从入口层(如 HTTP 请求处理入口、命令行参数解析入口)传递到业务逻辑层。这个传递过程可能跨越多个函数调用层级,如果每一层都要显式地传递模式参数,代码会变得很啰嗦。
解决这个问题的常见方案有三种:
- 显式参数传递:把模式作为参数逐层传递。优点是清晰、无魔法;缺点是侵入性强,每加一个模式相关参数都要改很多函数签名。
- 上下文对象:创建一个上下文对象,把模式和其他请求级信息(如用户身份、请求 ID)打包在一起,作为一个参数传递。这是我最推荐的方案,因为它在清晰性和便利性之间取得了很好的平衡。
- 线程本地存储 / 异步本地存储:把模式存储在 ThreadLocal 或 AsyncLocal 中,业务代码随时可以读取。优点是零侵入;缺点是隐式依赖,调试时不容易追踪模式的来源。
我个人的选择是:在框架层使用上下文对象,在业务层通过依赖注入获取模式。具体来说,入口层负责创建上下文对象并确定模式,然后把上下文对象注入到业务逻辑的依赖中。业务代码通过依赖注入拿到上下文对象,读取当前模式。这样既避免了逐层传递参数的繁琐,又保持了依赖的显式性。
class RequestContext: def __init__(self, mode, user_id, request_id): self.mode = mode self.user_id = user_id self.request_id = request_id class DataService: def __init__(self, context): self.context = context def process(self, data): if self.context.mode == ContextMode.STRICT: self._process_strict(data) elif self.context.mode == ContextMode.LENIENT: self._process_lenient(data) else: self._process_preview(data)4.4 第四步:为每个模式编写独立的测试用例
模式体系落地之后,测试策略也需要相应调整。传统的测试通常只覆盖"正常路径"和"异常路径",但在 context-mode 体系下,你需要为每个模式分别编写测试用例。
具体来说,对于每个模式,至少要覆盖以下几类测试:
| 测试类型 | 测试目标 | 示例 |
|---|---|---|
| 模式识别测试 | 验证入口层能正确识别模式 | 给定特定请求特征,断言模式为预期值 |
| 模式行为测试 | 验证每个模式下的核心行为符合定义 | 严格模式下遇到错误应中止,宽松模式下应跳过 |
| 模式边界测试 | 验证模式之间不会互相泄漏 | 在 A 模式下执行操作,断言 B 模式的逻辑未被触发 |
| 模式切换测试 | 验证模式切换时的状态清理 | 从严格模式切到宽松模式后,之前的严格状态不应残留 |
这些测试看起来工作量很大,但实际上它们带来的收益远超投入。因为 context-mode 体系最大的风险就是"模式行为不符合预期"和"模式之间互相干扰",而这些测试正好能覆盖这两类风险。
我在实际项目中还发现一个额外的好处:为每个模式编写测试的过程,往往会暴露出模式定义本身的问题。比如你发现某个模式的测试用例怎么写都很别扭,那很可能说明这个模式的边界定义得不够清晰,需要重新审视。
5. 几个容易踩的坑和对应的排查思路
5.1 模式判断逻辑散落在业务代码里
这是最常见的问题。虽然你定义了模式体系,但业务代码里还是到处在写if mode == 'strict'这样的判断。结果模式体系形同虚设,维护成本反而更高了——因为现在你既要维护模式定义,又要维护散落的判断逻辑。
排查这个问题的方法很简单:搜索代码库里所有直接比较模式值的地方。如果发现某个模式值的比较出现在业务逻辑层(而不是框架层或模式定义层),那就说明模式判断逻辑泄漏了。
修复的思路是:把模式相关的行为差异,封装到模式对象本身的方法里。比如不要写if mode == 'strict': validate_strictly(),而是写mode.validate(data),让模式对象自己决定怎么校验。这样业务代码就不需要知道具体有哪些模式,只需要调用模式对象的方法就行了。
5.2 模式在异步流程中丢失
在异步系统里,模式传递是一个特别容易出问题的地方。比如一个请求进来时确定了模式,但在处理过程中启动了一个异步任务,异步任务里读取模式时发现是空的或者默认值。
这个问题的根源在于,很多异步框架的上下文传递机制和同步代码不同。在同步代码里,你可以用线程本地存储来传递模式;但在异步代码里,线程可能会被复用,线程本地存储就不靠谱了。
解决方案是使用异步上下文传播机制。不同的语言和框架有不同的实现方式,但核心思路是一样的:把模式绑定到异步任务的执行上下文上,确保异步任务在执行时能拿到正确的模式。
import asyncio from contextvars import ContextVar current_mode = ContextVar('current_mode', default=ContextMode.LENIENT) async def handle_request(mode): current_mode.set(mode) await process_data() async def process_data(): mode = current_mode.get() # 这里的 mode 是正确的注意:使用 ContextVar 时,要确保在创建异步任务之前设置好模式,否则子任务可能拿到默认值。
5.3 模式与缓存/连接池的交互问题
这是一个比较隐蔽的坑。如果你的系统使用了缓存或连接池,而这些资源是跨模式共享的,就可能出现模式间的数据污染。
举个例子。假设严格模式下查询数据时会做额外的权限过滤,而宽松模式下不做。如果两个模式共享同一个缓存,那宽松模式下的查询结果可能被缓存起来,然后被严格模式读取到,导致权限过滤被绕过。
这类问题的排查思路是:检查所有跨请求共享的资源,确认它们是否与模式相关。如果相关,要么把模式作为缓存键的一部分,要么为每个模式维护独立的资源池。
我在一个项目里就遇到过这个问题。当时系统用一个全局缓存来存储用户权限信息,但不同模式下权限的计算方式不同。结果在预览模式下计算出的权限被缓存了,然后在正常模式下被使用,导致用户看到了不该看到的数据。修复方案是在缓存键里加上模式标识,确保不同模式的缓存互不干扰。
5.4 新增模式时的回归风险
每次新增一个模式,都有可能影响到现有模式的行为。因为新增模式往往意味着要修改模式判断的逻辑,而模式判断逻辑的修改很容易引入回归问题。
降低这个风险的关键是:让模式判断逻辑尽可能集中和简单。如果模式判断逻辑分散在多个地方,新增模式时就需要修改多个地方,回归风险自然就高。反之,如果模式判断逻辑集中在一处,新增模式时只需要改一个地方,风险就低得多。
另外,前面提到的"为每个模式编写独立测试用例"在这里也能发挥作用。每次新增模式后,跑一遍所有现有模式的测试用例,就能快速发现是否有回归问题。
6. 从 context-mode 延伸出的架构思考
6.1 模式与策略模式的区别与联系
学过设计模式的人可能会问:context-mode 和策略模式(Strategy Pattern)有什么区别?
简单来说,策略模式关注的是算法的可替换性——同一个问题,可以用不同的算法来解决,运行时选择用哪个算法。而 context-mode 关注的是行为的上下文相关性——同一套逻辑,在不同的上下文语境下应该表现出不同的行为。
两者的联系在于,context-mode 的实现经常会用到策略模式。每个模式可以看作一个策略,模式切换就是策略切换。但 context-mode 比策略模式多了一层"上下文"的含义:模式不仅仅是一组算法的选择,它还包含了错误处理策略、日志策略、权限策略等一系列与上下文相关的行为。
理解这个区别很重要,因为它影响你的设计思路。如果你把 context-mode 仅仅当作策略模式来实现,可能会忽略掉一些上下文相关的细节,比如模式的传递、模式的默认值、模式之间的互斥关系等。
6.2 模式体系的可观测性建设
模式体系上线之后,可观测性就变得非常重要。你需要能够回答这些问题:当前系统里各个模式的使用频率是多少?某个模式下的错误率是否异常?模式切换是否频繁?
这些信息对于运维和优化都至关重要。如果发现某个模式几乎没被使用,可能说明这个模式的设计有问题,或者入口层的模式识别逻辑有 bug。如果发现某个模式下的错误率明显高于其他模式,可能说明这个模式的配置需要调整。
建设可观测性的方式有很多,最基础的是在日志里记录模式信息,然后在日志分析系统里按模式维度做聚合。更进阶的做法是使用指标系统,为每个模式维护独立的计数器、直方图等指标。
from prometheus_client import Counter mode_requests = Counter('requests_by_mode', 'Request count by context mode', ['mode']) def handle_request(request): mode = determine_mode(request) mode_requests.labels(mode=mode.value).inc() # 后续处理6.3 模式体系的演进策略
最后想聊的是模式体系的演进。一个模式体系不是设计出来就一成不变的,它会随着业务的发展而不断演进。
演进的常见方向有三个:
- 模式细化:随着业务复杂度的提升,原本合并在一起的模式可能需要拆分。比如前面提到的
Batch拆成InteractiveBatch和ScheduledBatch。 - 模式合并:随着对业务理解的加深,发现某些模式的差异其实并不重要,可以合并。这种情况通常发生在系统重构或简化时。
- 模式废弃:某些模式随着业务变化不再需要,应该及时废弃。废弃模式时要注意清理相关的配置、测试和文档,避免留下"僵尸模式"。
演进的关键是保持模式定义的清晰和一致。每次调整模式体系时,都要同步更新模式定义、配置、测试和文档。如果只改代码不改文档,过不了多久模式体系就会变得混乱不堪。
我在实际项目中的做法是:把模式定义放在一个独立的模块里,所有与模式相关的配置、文档、测试都围绕这个模块组织。这样每次调整模式时,只需要关注这一个模块,不容易遗漏。
7. 一些实操中的个人体会
聊了这么多理论和方法,最后分享几个我在实际落地 context-mode 过程中总结的个人体会。
第一个体会是:不要追求一步到位。我见过一些团队,一上来就想设计一个完美的模式体系,结果花了大量时间讨论模式怎么划分,却迟迟没有落地。我的建议是先定义三五个核心模式,快速落地,然后在实际使用中逐步调整。模式体系的价值在于使用,不在于设计得有多完美。
第二个体会是:模式的命名非常重要。一个好的模式名应该让人一眼就能看出这个模式的语义,而不是需要去看文档才能理解。我见过一些模式命名,比如ModeA、ModeB、Type1、Type2,这种命名完全没有信息量,读代码的人根本不知道这些模式是干什么的。好的命名应该像StrictValidation、PreviewOnly、BatchProcessing这样,名字本身就传达了模式的语义。
第三个体会是:模式的数量要克制。模式不是越多越好。每增加一个模式,就增加了一份维护成本。如果一个模式只在一个地方使用,而且行为差异很小,那可能不值得单独定义一个模式。我通常会把模式数量控制在十个以内,超过十个就会重新审视是否有合并的空间。
第四个体会是:模式体系需要文档,但文档不能替代代码。我见过一些团队,模式的定义只存在于文档里,代码里没有任何体现。结果文档很快就过时了,新来的人看文档以为系统是这样的,看代码发现完全不是。正确的做法是让模式定义在代码里显式存在,文档只是对代码的补充说明。
第五个体会是:模式切换的日志要详细。每次模式切换都应该记录日志,包括切换前后的模式、切换的原因、切换的触发点。这些日志在排查问题时非常有用。我遇到过一个 bug,表现是某个操作偶尔会失败,排查了很久才发现是因为模式在某个异步回调里被意外切换了。如果当时有详细的模式切换日志,这个问题可能几分钟就能定位。
最后一个体会是:模式体系不是银弹。它解决的是"同一套逻辑在不同上下文下行为不同"的问题,但并不是所有场景都适合用模式体系来解决。如果一个系统的上下文差异很小,或者上下文数量很少,那用简单的条件判断可能更直接。模式体系的价值在于管理复杂性,如果复杂性本身不高,引入模式体系反而增加了不必要的抽象。
这些体会都是在实际项目中踩过坑之后总结出来的,希望对正在考虑落地 context-mode 的你有所帮助。每个系统的情况不同,具体怎么落地还需要结合实际情况来判断,但核心原则是通用的:让上下文显式化,让模式有边界,让行为可预测。