TypeScript中避免多调用方修改共享状态的方案咨询
问题背景
现有多个实现了IAdapter接口并实现do方法的类:AdapterA、AdapterB、AdapterC。Context类型定义如下:
type Context = { obj: BigObjectNested; client: rpcClient; logFetcher: LogFetcher; message: string; } interface IAdapter { do(ctx: Context): void; }
在Service.ts中会创建一个Context对象,并将其传递给每个适配器的do方法。当前存在的问题是:各适配器相互独立,但如果某个适配器修改了Context,会影响后续适配器的使用。
我设想了三种解决方案:
- 调用每个适配器的
do方法前克隆Context,但适配器数量较多时操作繁琐; - 使用
readonly类型无法防护嵌套对象的修改,因此可以改用DeepReadOnly; - 为每个适配器创建包含新
LogFetcher、client等实例的Context,但存在适配器过多导致类数量膨胀的问题,且Service.ts构造函数接收的实例仍需克隆。
问题1
上述三种方案中哪个最适配该场景?所选方案存在哪些问题及优化建议?
问题2
通用场景下,比如UserService构造函数接收DbService单例,readonly无法阻止修改DbService内部属性,业界的最佳实践是什么?
问题1解答
三种方案里,**方案1(调用前克隆Context)**是最适配当前场景的,原因如下:
- 方案2的
DeepReadOnly仅能在编译阶段提供防护,运行时如果适配器通过类型断言绕过只读限制,依然能修改Context内容,无法从根本上解决运行时的副作用问题; - 方案3为每个适配器单独创建Context会大幅增加代码复杂度,类数量膨胀的问题会让后续维护成本飙升,完全得不偿失。
方案1存在的问题及优化建议
- 现有问题:适配器数量多的时候,手动逐个克隆会重复代码,显得繁琐。
- 优化建议:
- 封装一个通用的Context克隆工具函数,比如
cloneContext(ctx: Context): Context,内部处理深层克隆逻辑(可以用结构化克隆、自定义递归克隆实现); - 在Service中实现一个批量调用适配器的方法,内部自动为每个适配器克隆Context,示例代码:
这样不管适配器数量多少,都不需要手动重复克隆操作;class Service { private adapters: IAdapter[]; constructor(adapters: IAdapter[]) { this.adapters = adapters; } process(ctx: Context) { this.adapters.forEach(adapter => { const clonedCtx = cloneContext(ctx); adapter.do(clonedCtx); }); } } - 如果Context中的某些属性是不可变的单例(比如rpcClient、logFetcher),可以采用浅克隆+嵌套对象深克隆的混合模式,提升克隆效率,避免不必要的单例实例复制。
- 封装一个通用的Context克隆工具函数,比如
问题2解答
针对单例依赖内部属性被修改的问题,业界最佳实践主要有以下几种:
- 依赖隔离:使用接口而非具体类注入
定义一个仅包含必要方法的接口(比如IDbService),只暴露查询、更新等业务方法,不暴露内部属性。UserService依赖这个接口而非DbService具体类,即使DbService内部有可变属性,外部也无法直接访问修改。 - 不可变设计:将单例的内部属性设为只读
在DbService内部把所有可变属性都定义为readonly,或者通过私有字段+getter方法暴露,禁止外部直接修改。示例代码:class DbService { private readonly config: DbConfig; constructor(config: DbConfig) { this.config = {...config}; // 构造时就做一次克隆,避免外部传入的对象被修改影响内部 } getConfig(): Readonly<DbConfig> { return this.config; } } - 防御性复制:在单例的方法中返回数据副本
如果单例需要返回内部状态数据,不要直接返回引用,而是返回克隆后的副本,避免外部通过引用修改内部属性。 - 依赖容器的生命周期管理
如果业务场景允许,不为所有组件共享同一个单例,而是为需要隔离的组件创建独立的实例(比如请求级别的实例),但这种方式要根据实际业务场景权衡资源消耗。
内容的提问来源于stack exchange,提问作者Giorgi Lagidze
相关产品推荐
相关产品推荐

