You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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),可以采用浅克隆+嵌套对象深克隆的混合模式,提升克隆效率,避免不必要的单例实例复制。

问题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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.02 04:04:53