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

Angular Signals中嵌套对象不可变更新的最佳实践咨询

问题描述

我定义了两个类:

export class Enrollment {
    private _productId = signal(0);
    private _price = signal(0);
    
    public productId = this._productId.asReadonly();
    public price = this._price.asReadonly();

    public setProductId(productId: number): void {
        this._productId.set(productId);
    }

    public setPrice(price: number): void {
        this._price.set(price);
    }
}

export class Cart {
    private _enrollments = signal<Enrollment[]>([]);
    
    public enrollments = this._enrollments.asReadonly();

    public addToCart(enrollment: Enrollment): void {
        this._enrollments.set([...this._enrollments(), enrollment]);
    }

    public removeFromCart(enrollment: Enrollment): void {
        this._enrollments.set(this._enrollments().filter(e => e !== enrollment));
    }

    public emptyCart(): void {
        this._enrollments.set([]);
    }
}

我正在寻找更新_enrollments数组中特定Enrollment实例内部数据的最佳方式。目前想到两种方案:

  • 直接修改Enrollment对象:在Enrollment类中定义equals方法,通过公共方法直接修改实例,触发依赖该实例数据的信号更新。
  • 创建不可变的Enrollment更新:将所有Enrollment操作逻辑写在Cart服务中,实现不可变更新。但这种方式不够理想。

请问是否存在一种既能保持不可变性,又符合关注点分离原则的更优实现方案?


最优方案建议

可以采用不可变Enrollment实例 + 单一职责更新逻辑的组合方式,同时满足不可变性和关注点分离的要求:

1. 重构Enrollment为不可变类

去掉类内部的setter方法,改为通过静态/实例方法生成新实例,确保每个Enrollment一旦创建就不可修改,将属性更新的逻辑封装在自身内部:

export class Enrollment {
    private readonly _productId: number;
    private readonly _price: number;
    
    public get productId(): number {
        return this._productId;
    }
    
    public get price(): number {
        return this._price;
    }

    constructor(productId: number, price: number) {
        this._productId = productId;
        this._price = price;
    }

    // 生成新实例的方法,替代修改原有实例
    public withProductId(newProductId: number): Enrollment {
        return new Enrollment(newProductId, this._price);
    }

    public withPrice(newPrice: number): Enrollment {
        return new Enrollment(this._productId, newPrice);
    }

    // 提供匹配逻辑,用于在Cart中定位目标实例
    public matches(productId: number): boolean {
        return this._productId === productId;
    }
}

2. 在Cart中实现不可变数组更新

Cart只负责管理Enrollment数组的替换,不介入Enrollment内部的属性修改逻辑,专注于数组层面的操作:

export class Cart {
    private _enrollments = signal<Enrollment[]>([]);
    
    public enrollments = this._enrollments.asReadonly();

    public addToCart(enrollment: Enrollment): void {
        this._enrollments.set([...this._enrollments(), enrollment]);
    }

    public removeFromCart(enrollment: Enrollment): void {
        this._enrollments.set(this._enrollments().filter(e => e !== enrollment));
    }

    public emptyCart(): void {
        this._enrollments.set([]);
    }

    // 通用更新方法,通过回调委托Enrollment的属性修改逻辑
    public updateEnrollment(productId: number, updateFn: (enrollment: Enrollment) => Enrollment): void {
        this._enrollments.set(
            this._enrollments().map(enrollment => 
                enrollment.matches(productId) ? updateFn(enrollment) : enrollment
            )
        );
    }
}

3. 使用示例

调用时通过Cart的updateEnrollment方法,结合Enrollment的不可变更新方法完成修改:

const cart = new Cart();
cart.addToCart(new Enrollment(1, 100));

// 更新productId为1的Enrollment价格
cart.updateEnrollment(1, (e) => e.withPrice(150));

方案优势

  • 不可变性:所有更新都生成新的Enrollment实例,原有实例不会被修改,避免意外副作用。
  • 关注点分离:Enrollment负责自身属性的不可变更新逻辑,Cart只负责数组的管理,职责边界清晰。
  • 灵活性:updateEnrollment通过回调函数支持任意属性的更新,无需为每个属性单独编写方法。

内容的提问来源于stack exchange,提问作者Alex Dantsev

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.22 16:05:01