1. 从"上下文模式"说起:一个被低估的工程概念
第一次看到"context-mode"这个词,很多人会下意识地把它归类成某个框架里的配置项,或者某个编辑器插件的开关。但如果你真正在工程一线待过几年,就会发现这个词背后藏着一整套关于"状态管理"和"执行环境切换"的设计哲学。它不是一个具体的库,也不是某个产品的专属名词,而是一种贯穿在系统设计、应用开发、甚至日常工具使用中的思维模式。
我最早接触这个概念,是在处理一个多租户后台系统的时候。当时系统需要在同一套代码里同时服务三类用户:普通访客、登录用户、管理员。每类用户看到的界面、能调用的接口、甚至数据缓存的策略都不一样。最初的做法是到处写 if-else,代码很快就变成了一团乱麻。后来团队引入了一个"上下文模式"的思路,把当前用户的身份、权限、环境配置打包成一个上下文对象,所有业务逻辑都基于这个上下文来决策。代码量直接砍掉了三分之一,可维护性提升了一个档次。
这就是 context-mode 的核心价值:把"当前处于什么状态"这件事从散落各处的判断逻辑中抽离出来,变成一个显式的、可传递的、可切换的上下文对象。它解决的问题很具体——当系统需要在多种模式之间切换时,如何避免状态污染、如何保证切换的一致性、如何让每一段业务代码都能清楚地知道自己"现在在哪"。
这篇文章适合几类人看:一是正在做多环境、多角色、多租户系统的后端或全栈工程师;二是做前端状态管理时被各种全局变量折磨过的开发者;三是对架构设计感兴趣、想理解"模式切换"这类通用问题怎么优雅解决的技术爱好者。哪怕你只是写写脚本、做做自动化,理解 context-mode 的思路也能让你的代码干净不少。
接下来我会从设计思路、核心细节、实操落地、问题排查几个维度,把这个概念彻底拆开讲清楚。不是泛泛而谈,而是把我踩过的坑、试过的方案、最终沉淀下来的做法都摊开来说。
2. 上下文模式的整体设计与思路拆解
2.1 为什么需要"模式"而不是"参数"
很多人会问:我直接传参数不就行了吗?为什么要搞一个"模式"的概念?这个问题的答案,决定了你对 context-mode 的理解深度。
传参数的方式,本质上是把决策权交给调用方。每个函数都需要知道自己需要哪些参数,调用方需要清楚地知道每个函数要什么。当系统简单的时候,这没问题。但当系统复杂到一定程度,比如一个请求要经过十几层调用,每一层都需要知道"当前用户是谁""当前是什么环境""当前是否处于调试状态",你就会发现参数列表爆炸了。更糟糕的是,中间某一层可能根本不需要这些信息,但为了传给下一层,它不得不把这些参数接过来再传下去。这就是典型的"参数穿透"问题。
上下文模式的做法完全不同。它把这些"全局性但又不是真正全局"的信息打包成一个对象,放在一个所有层都能访问到的地方。每一层需要的时候自己去取,不需要的时候完全不用关心。这个对象就是"上下文",而"模式"则描述了上下文当前处于哪种状态组合。
用一个生活化的类比:参数传递像是每次出门都要把身份证、钥匙、钱包、手机一个个塞进口袋,还要记得每个场合该带哪些。上下文模式则像是背了一个包,包里什么都有,需要什么掏什么。包本身是统一的,里面的东西可以按场景调整。
2.2 上下文模式的三层结构
一个设计良好的 context-mode 系统,通常包含三个层次,我把它叫做"三层结构":
第一层是上下文载体。这是存放状态的地方,可能是一个对象、一个 Map、一个线程局部变量,或者框架提供的某种作用域机制。载体的选择直接决定了上下文的生命周期和隔离性。比如在 Web 后端,请求级别的上下文通常挂在请求对象上;在异步任务里,可能需要用协程上下文或者显式的传递。
第二层是模式定义。模式是对上下文状态的一种分类或标签。比如"开发模式""生产模式""只读模式""维护模式"。模式定义清楚了,业务代码就可以基于模式做分支,而不是基于一堆零散的状态字段。这一步的价值在于把隐式的状态组合显式化。
第三层是切换机制。上下文不是一成不变的,它需要在合适的时机切换。切换机制要解决两个问题:什么时候切、怎么切才能保证一致性。切早了状态没准备好,切晚了业务已经执行了。切换过程中如果出现异常,还要能回滚。
这三层缺一不可。我见过很多项目只做了第一层,把状态塞进一个全局对象就完事了,结果模式判断散落各处,切换逻辑到处都是,最后比传参数还乱。也见过只做模式定义不做切换机制的,模式之间互相污染,调试起来痛不欲生。
2.3 方案选型:几种常见实现路径的取舍
实现 context-mode 有好几条路,每条路适合的场景不一样。我把常见的几种列出来对比一下:
| 实现方式 | 隔离性 | 传递便利性 | 适用场景 | 主要风险 |
|---|---|---|---|---|
| 全局单例 | 差 | 极好 | 单进程单任务脚本 | 并发下状态互相覆盖 |
| 线程局部变量 | 中 | 好 | 多线程同步服务 | 线程池复用导致状态残留 |
| 请求作用域对象 | 好 | 中 | Web 请求处理 | 异步分支容易丢失 |
| 显式参数传递 | 极好 | 差 | 简单调用链 | 参数穿透,代码冗长 |
| 框架上下文机制 | 好 | 好 | 使用成熟框架的项目 | 学习成本,黑盒行为 |
选哪条路,取决于你的系统并发模型和调用链复杂度。我的经验是:并发越高、调用链越深,越应该用框架提供的上下文机制或者请求作用域对象;反之,简单脚本用全局单例就够了,别过度设计。
这里有个容易被忽略的点:上下文的"可见范围"和"生命周期"必须匹配。请求级别的上下文不能活到请求结束之后,否则就是内存泄漏;任务级别的上下文不能跨任务共享,否则就是状态污染。很多线上事故的根因,就是这两者没对齐。
2.4 上下文模式带来的实际收益
说完了设计,聊聊收益。我在实际项目里推行 context-mode 之后,观察到的变化主要有这么几个:
代码可读性提升最明显。以前一个函数开头要写五六个参数校验,现在只需要从上下文里取一次,后面逻辑清爽很多。新人接手代码的时候,理解成本也低了不少,因为"当前处于什么模式"这件事是显式的,不用去猜。
测试变得容易。因为上下文是一个可以构造的对象,测试的时候可以随意构造各种模式组合,不需要真的去模拟复杂的运行环境。以前测一个"管理员在维护模式下"的场景,要搭一堆环境,现在直接构造一个上下文对象就行。
扩展性变好。新增一种模式,只需要在模式定义里加一项,在切换机制里加一条规则,业务代码基本不用动。这比到处改 if-else 强太多了。
当然,收益不是白来的。上下文模式引入了额外的抽象层,如果设计不当,会变成"为了模式而模式",反而增加复杂度。所以下一节我会重点讲核心细节和实操要点,把怎么设计好这件事说透。
3. 核心细节解析与实操要点
3.1 上下文载体的选择与初始化时机
上下文载体的选择,是整个设计的地基。选错了,后面怎么补都别扭。
先说初始化时机。上下文必须在业务逻辑执行之前就准备好,而且只能初始化一次。我见过有项目在业务执行过程中懒加载上下文,结果第一层调用时上下文是空的,第二层才填上,导致第一层的行为和第二层不一致。这种 bug 特别隐蔽,因为大部分时候能跑通,只在特定路径下才暴露。
正确的做法是:在请求进入系统的最外层,或者任务开始执行的最前面,就把上下文构造好。构造的时候要明确哪些字段是必填的,哪些是可选的。必填字段缺失就直接拒绝,不要让它带着不完整的上下文往下走。
再说载体本身。以常见的 Web 后端为例,请求作用域对象是最自然的选择。大多数框架都提供了在请求对象上挂载自定义数据的能力,或者提供了专门的上下文容器。用这个的好处是生命周期天然和请求对齐,请求结束上下文自动销毁,不用担心泄漏。
如果是异步任务或者消息消费场景,情况会复杂一些。异步任务可能没有明确的"请求"边界,这时候需要显式地创建上下文,并且在任务结束时显式地清理。清理这一步千万别省,我踩过的坑就是任务执行完忘了清理,下一个任务复用了同一个上下文对象,读到了上一个任务的残留状态,排查了大半天。
提示:上下文载体的生命周期一定要和业务单元的生命周期严格对齐。请求对应请求,任务对应任务,会话对应会话。对齐了,很多并发问题自然就没了。
3.2 模式定义的粒度控制
模式定义太粗,起不到隔离作用;太细,又会导致模式爆炸。这个度怎么把握?
我的经验是:按"行为差异"来定义模式,而不是按"状态字段"来定义。什么意思呢?如果两种状态组合下,业务代码的执行路径完全一样,那它们就不需要是两个模式。只有当业务代码需要根据模式做出不同决策时,这个模式才有存在的意义。
举个例子。一个内容管理系统,可能有"草稿""待审核""已发布""已下架"几种状态。如果业务逻辑对"草稿"和"待审核"的处理完全一样,那它们可以合并成一个模式叫"未发布"。但如果审核流程对这两者有不同要求,那就必须分开。
模式的数量控制在个位数比较理想。超过十个,就要反思是不是定义得太细了。我见过一个项目定义了二十多种模式,结果模式判断逻辑比业务逻辑还复杂,完全本末倒置。
另外,模式之间最好是互斥的。如果一个上下文同时属于多个模式,判断逻辑就会变得模糊。如果确实需要多维度的状态,可以用"模式 + 标签"的组合方式,模式表示主要状态,标签表示附加属性。这样既保持了模式的清晰,又能表达复杂的组合。
3.3 切换机制的设计要点
切换机制是 context-mode 里最容易出问题的地方。我总结了几条设计要点:
切换必须是原子的。要么全部切换成功,要么完全不切。不能出现切了一半的中间状态。实现上,可以先构造好新的上下文,验证通过后再整体替换,而不是逐个字段修改。
切换要有明确的触发点。什么时候切换,必须由明确的业务事件驱动,而不是靠某个条件判断碰巧成立。比如"用户登录成功后切换到登录模式""进入维护窗口后切换到维护模式"。触发点清晰了,切换的时机就不会乱。
切换要考虑并发。如果多个执行单元共享同一个上下文载体,切换的时候必须加锁或者用其他同步机制。否则一个单元正在读上下文,另一个单元把它切了,读到的就是脏数据。这个问题在单机多线程环境下特别常见。
切换要可回滚。切换过程中如果后续步骤失败,要能把上下文恢复到切换前的状态。实现上可以保存切换前的快照,失败时还原。这一步在涉及外部资源(比如数据库连接、文件句柄)切换时尤其重要。
3.4 业务代码如何优雅地消费上下文
上下文准备好了,模式定义好了,切换机制也有了,最后一步是业务代码怎么用。这一步做不好,前面的努力全白费。
核心原则是:业务代码只读上下文,不写上下文。上下文的修改只能通过切换机制来做。如果业务代码可以随意修改上下文,那上下文就退化成了一个全局变量,隔离性荡然无存。
读取的时候,建议封装成语义化的方法,而不是直接访问字段。比如context.isReadOnly()比context.mode === 'readonly'更好,因为前者把判断逻辑收敛到了一处,将来模式定义变了,只需要改这一个方法。
还有一个技巧:把常用的上下文判断提取成"守卫"或者"装饰器"。比如在方法上加一个@requiresMode('admin')的注解,方法执行前自动检查模式,不匹配就拒绝。这样业务代码里连判断都省了,模式检查变成了声明式的。
注意:不要在上下文里存放业务数据。上下文只放"环境信息"和"模式状态",业务数据该走参数走参数,该走数据库走数据库。把业务数据塞进上下文,会让上下文变得臃肿,也会让数据流向变得不清晰。
4. 实操过程与核心环节实现
4.1 一个完整的上下文模式实现示例
光说理论不够,我拿一个具体的场景把整个流程走一遍。场景是这样的:一个数据处理服务,需要支持三种运行模式——"正常模式"处理全部数据,"采样模式"只处理一部分数据用于调试,"演练模式"只记录不实际写入。三种模式通过配置或者运行时指令切换。
先定义上下文载体。用 Python 举例,因为它的表达比较直观:
from dataclasses import dataclass, field from enum import Enum from typing import Optional class RunMode(Enum): NORMAL = "normal" SAMPLING = "sampling" DRY_RUN = "dry_run" @dataclass class Context: mode: RunMode = RunMode.NORMAL sample_rate: float = 1.0 operator: Optional[str] = None trace_id: Optional[str] = None _snapshot: Optional[dict] = field(default=None, repr=False) def is_normal(self) -> bool: return self.mode == RunMode.NORMAL def is_sampling(self) -> bool: return self.mode == RunMode.SAMPLING def is_dry_run(self) -> bool: return self.mode == RunMode.DRY_RUN def should_process(self, index: int) -> bool: if self.is_normal(): return True if self.is_sampling(): return (index % int(1 / self.sample_rate)) == 0 return False这个载体里,模式是核心字段,采样率是模式的参数,操作人和追踪 ID 是辅助信息。should_process方法把"当前模式是否应该处理这条数据"的判断收敛到了一处,业务代码只需要调用它。
接下来是切换机制。切换要原子、可回滚:
class ContextManager: def __init__(self): self._context = Context() @property def context(self) -> Context: return self._context def switch_mode(self, new_mode: RunMode, **kwargs): old_snapshot = { "mode": self._context.mode, "sample_rate": self._context.sample_rate, } try: self._context.mode = new_mode if new_mode == RunMode.SAMPLING: rate = kwargs.get("sample_rate", 0.1) if not (0 < rate <= 1): raise ValueError("sample_rate must be in (0, 1]") self._context.sample_rate = rate else: self._context.sample_rate = 1.0 except Exception: self._context.mode = old_snapshot["mode"] self._context.sample_rate = old_snapshot["sample_rate"] raise切换的时候先保存快照,修改过程中如果出错就还原。这样保证了要么切换成功,要么保持原状。
业务代码消费上下文的样子:
def process_batch(manager: ContextManager, data: list): ctx = manager.context processed = 0 skipped = 0 for i, item in enumerate(data): if not ctx.should_process(i): skipped += 1 continue if ctx.is_dry_run(): log_only(item) else: write_to_storage(item) processed += 1 return {"processed": processed, "skipped": skipped, "mode": ctx.mode.value}业务代码里没有任何if mode == 'xxx'的硬编码判断,全部通过上下文的方法来表达。将来新增一种模式,只需要改上下文类,业务代码基本不动。
4.2 参数计算与选择过程
上面例子里有个采样率的计算,这里展开说一下。采样模式的核心是"按比例处理数据",采样率决定了处理多少。
假设采样率是 0.1,意味着处理 10% 的数据。实现上有两种思路:一种是随机采样,每条数据以 10% 的概率被处理;另一种是间隔采样,每隔 10 条处理 1 条。我选了间隔采样,因为它的处理量是确定的,便于预估资源消耗和验证结果。随机采样虽然更"公平",但处理量会波动,调试的时候不好对账。
间隔采样的计算是index % int(1 / sample_rate) == 0。当 sample_rate 是 0.1 时,1 / 0.1 = 10,所以每 10 条处理第 0、10、20 条。这里有个坑:浮点数除法可能不精确,比如1 / 0.3得到的是 3.333...,取整后是 3,实际采样率变成了 1/3 而不是 0.3。所以更稳妥的做法是用整数表示采样间隔,比如直接配置"每 N 条采样一条",避免浮点误差。
我在实际项目里最终改成了配置采样间隔而不是采样率,配置项从sample_rate: 0.1变成了sample_interval: 10。这样既避免了浮点问题,配置也更直观。这个改动看起来小,但省去了很多解释成本——运维同学一看就懂,不用去算 0.1 到底是多少。
4.3 实操现场记录:从单机到并发的演进
最初这个服务是单机单线程跑的,上下文用全局单例完全没问题。后来数据量上来了,改成了多线程处理,问题就来了。
第一个问题是上下文被多个线程共享,一个线程切换模式,其他线程也跟着变了。这显然不对,每个线程应该有自己的模式。解决办法是把上下文从全局单例改成线程局部变量。Python 里用threading.local()就能实现。
第二个问题是线程池复用导致的状态残留。线程处理完一个任务后,上下文没有清理,下一个任务复用这个线程时,读到了上一个任务的模式。解决办法是在任务开始前重置上下文,或者在任务结束后清理。我选了前者,因为"开始前重置"更保险,不依赖任务正常结束。
第三个问题是异步分支丢失上下文。有些处理逻辑会派发子任务到线程池,子任务默认拿不到父任务的上下文。解决办法是显式地把上下文传递给子任务,或者在子任务创建时复制一份上下文。这里要注意是"复制"而不是"共享",否则子任务修改上下文会影响父任务。
这一路演进下来,我的体会是:上下文模式的设计必须提前考虑并发模型。如果一开始就按单线程设计,后面改并发的时候要动的地方很多。反过来,如果一开始就按并发设计,单线程场景下也不会有额外负担,只是多了一层线程局部变量的包装而已。
4.4 配置驱动的模式切换
模式切换的触发方式,我推荐配置驱动而不是硬编码。硬编码的切换逻辑散落在代码里,改起来麻烦,也容易漏。配置驱动的话,切换规则集中在一处,改配置就能调整行为。
配置可以是一个简单的规则表:
| 触发条件 | 目标模式 | 附加参数 |
|---|---|---|
| 环境变量 DEBUG=true | 采样模式 | 采样间隔 100 |
| 收到指令 dry_run | 演练模式 | 无 |
| 维护窗口开始 | 只读模式 | 无 |
| 默认 | 正常模式 | 无 |
服务启动时读取配置,运行时根据事件匹配规则,匹配到就切换。这样运维同学不需要改代码就能调整模式,开发同学也不用为每种切换场景写专门的逻辑。
配置驱动还有个好处:可以记录切换历史。每次切换都记一条日志,包括切换时间、触发条件、切换前后的模式。出问题的时候,翻日志就能知道当时是什么模式,省去了大量猜测。
5. 常见问题与排查技巧实录
5.1 上下文丢失的几种典型场景
上下文丢失是最高频的问题。表现是业务代码读上下文时拿到空值或者默认值,导致行为不符合预期。常见的丢失场景有这么几种:
异步分支丢失。主流程有上下文,派发到线程池或者协程的子任务没有。排查方法是看子任务的创建点,确认上下文有没有显式传递。解决方法是创建子任务时复制上下文。
跨服务调用丢失。服务 A 有上下文,调用服务 B 时没有把上下文带过去。这个要看调用协议,HTTP 调用可以把上下文关键字段放在请求头里,RPC 调用可以放在附件里。注意只传必要字段,不要把整个上下文序列化过去,既浪费带宽又有安全风险。
中间件重置丢失。某些框架的中间件会重置请求作用域,导致之前设置的上下文被清掉。排查方法是看中间件的执行顺序,确认上下文设置和中间件执行的先后关系。
序列化丢失。上下文对象如果被序列化再反序列化,某些字段可能丢失,比如不可序列化的对象引用。排查方法是检查上下文的字段类型,确保都是可序列化的。
5.2 模式判断错误的排查思路
模式判断错误的表现是业务代码走了错误的分支。排查的时候按这个顺序来:
先确认当前模式是什么。在业务代码入口打一条日志,把上下文的模式打出来。如果日志显示的模式和预期不符,问题在切换环节;如果模式对但行为不对,问题在判断逻辑。
再确认切换是否发生。看切换日志,确认切换的触发条件和时机。有时候切换发生了但被后续操作覆盖了,日志能看出来。
然后确认判断逻辑。把模式判断的代码单独拎出来测,用各种模式组合跑一遍,看结果是否符合预期。这一步能排除判断逻辑本身的 bug。
最后确认并发影响。如果是多线程环境,可能是其他线程切换了模式。这时候要看上下文的隔离性是否到位,线程局部变量有没有正确使用。
5.3 常见问题速查表
| 问题现象 | 可能原因 | 排查方法 | 解决方案 |
|---|---|---|---|
| 上下文读到空值 | 异步分支未传递 | 检查子任务创建点 | 创建时复制上下文 |
| 模式与预期不符 | 切换未触发或被覆盖 | 查看切换日志 | 修正触发条件,加锁 |
| 状态残留 | 上下文未清理 | 检查任务结束逻辑 | 任务开始前重置 |
| 并发下行为异常 | 上下文被共享 | 检查隔离机制 | 改用线程局部变量 |
| 切换后部分生效 | 切换非原子 | 检查切换代码 | 先构造后整体替换 |
| 跨服务模式不一致 | 上下文未传递 | 检查调用协议 | 关键字段放请求头 |
5.4 几个我踩过的坑
第一个坑是上下文里放了可变对象。当时上下文里存了一个列表,业务代码往列表里加东西,结果多个请求共享了同一个列表,数据串了。教训是上下文里尽量放不可变对象,必须放可变的就每次深拷贝。
第二个坑是模式切换没有考虑幂等。同一个切换指令发了两次,第二次切换的时候上下文已经是目标模式了,但切换逻辑还是执行了一遍,触发了不必要的副作用。教训是切换前先判断当前模式是否已经是目标模式,是的话直接返回。
第三个坑是上下文清理放在了 finally 里但 finally 本身抛异常。清理逻辑如果抛异常,会掩盖真正的业务异常。教训是清理逻辑要包一层 try-except,清理失败只记日志不抛异常。
第四个坑是模式定义用了字符串字面量。代码里到处是if mode == "normal",后来改模式名的时候漏改了一处,出了 bug。教训是模式定义用枚举或者常量,让编译器或者静态检查帮忙发现问题。
提示:上下文模式看起来简单,但魔鬼都在细节里。上面这几个坑,每一个都让我花了半天以上去排查。提前知道能省很多时间。
6. 上下文模式的扩展玩法与个人体会
6.1 从运行模式扩展到更多维度
context-mode 的思路不局限于"运行模式"这一个维度。它可以扩展到很多场景:
权限维度。把用户角色、权限列表放进上下文,业务代码基于权限做判断。这其实就是很多权限框架的做法,只是换了个名字。
租户维度。多租户系统里,当前租户的信息放进上下文,数据查询自动带上租户过滤条件。这样业务代码不用每次都手动加租户条件,减少遗漏。
地域维度。全球化服务里,当前请求的地域放进上下文,决定用哪个数据中心、哪种语言、哪种货币。业务代码基于地域做本地化。
实验维度。A/B 测试里,当前用户属于哪个实验组放进上下文,业务代码基于实验组走不同逻辑。这样实验逻辑和业务逻辑解耦,实验结束直接下线配置就行。
这些维度的共同点是:都是"全局性但非真正全局"的信息,都需要在调用链中传递,都需要在特定时机切换。用 context-mode 统一处理,比每个维度各搞一套要清爽得多。
6.2 上下文模式与依赖注入的关系
有人会问:上下文模式和依赖注入是不是重复了?我的看法是:它们解决的是不同问题,但可以配合使用。
依赖注入解决的是"对象怎么拿到它依赖的其他对象",关注的是对象之间的组装关系。上下文模式解决的是"对象怎么知道当前处于什么状态",关注的是运行时的环境信息。
一个对象可以通过依赖注入拿到一个上下文访问器,然后通过这个访问器读取上下文。这样既保持了依赖关系的清晰,又获得了上下文模式的便利。两者不是竞争关系,而是互补关系。
我在实际项目里就是这么做的:上下文管理器通过依赖注入提供给需要的组件,组件不直接持有上下文,而是持有管理器,需要的时候从管理器取。这样上下文的生命周期由管理器统一控制,组件不用关心。
6.3 什么时候不该用上下文模式
不是所有场景都适合上下文模式。以下几种情况,我建议直接用参数传递,别引入上下文:
调用链很浅。如果只有两三层调用,参数传递完全够用,引入上下文反而增加理解成本。
状态很少。如果只有一两个状态字段,而且变化不频繁,直接传参数更直接。
并发模型简单。单线程、单任务的脚本,全局变量就够了,上下文模式是杀鸡用牛刀。
团队不熟悉。如果团队里没人用过上下文模式,引入之前要先做技术分享,否则容易用歪。我见过团队照猫画虎搞了一套上下文,结果因为不理解原理,用出了各种问题,最后又退回了参数传递。
判断标准很简单:当"参数穿透"带来的痛苦超过了"引入抽象"带来的成本时,才值得用上下文模式。这个临界点因项目而异,需要自己判断。
6.4 我个人的几点体会
用了几年上下文模式,最大的体会是:它本质上是一种"显式化"的努力。把原本隐式散落在各处的状态,显式地收敛到一个对象里。这个收敛过程本身,就会逼着你思考"到底有哪些状态""它们之间是什么关系""什么时候该变"。很多时候,光是这个思考过程,就能发现设计上的问题。
第二个体会是:上下文模式的价值在系统复杂度上升期才体现出来。系统简单的时候,它看起来是多余的。但当系统从简单走向复杂,从单机走向分布式,从单一模式走向多模式,它的价值就凸显了。所以引入的时机很重要,太早是过度设计,太晚是积重难返。
第三个体会是:上下文模式不是银弹。它解决了一类问题,但引入了另一类问题,比如并发隔离、生命周期管理、调试复杂度。用之前要权衡,用之后要持续维护。我见过用上下文模式用得很好的项目,也见过用得一塌糊涂的项目,差别不在模式本身,而在用的人有没有真正理解它。
最后一个体会是关于"模式"这个词。模式意味着分类,分类意味着简化。但现实往往是连续的、模糊的,硬要分成几个离散的模式,有时候会失真。所以模式的定义要留有余地,允许"混合模式"或者"模式加标签"的存在。不要为了模式的纯粹性,牺牲了对现实的准确表达。
这套东西说到底,是工程上的一种权衡。没有绝对的对错,只有适不适合。希望我这些经验能帮你少走点弯路,在自己的项目里把 context-mode 用好。