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
相关产品推荐
相关产品推荐

