不可变对象引用更新:如何同步关联对象的属性变更?
解决方案:避免Customer变更同步遗漏的两种思路
一、更优的替代方案:间接引用代理模式
既然Customer是不可变类,我们可以放弃“修改原Customer实例”的思路,转而通过代理类间接持有Customer引用——所有需要关联Customer的业务类(Order、Invoice等)都持有这个代理,而非直接持有Customer实例。当客户信息变更时,创建新的Customer实例并更新代理内的引用,所有关联类会自动获取到最新数据,根本不需要事件通知。
代码示例
// 不可变Customer类(保持原有设计) public sealed class Customer { public string Id { get; } public string PhoneNumber { get; } public Customer(string id, string phoneNumber) { Id = id; PhoneNumber = phoneNumber; } // 不可变类的属性修改方式:返回新实例 public Customer WithPhoneNumber(string newPhone) { return new Customer(Id, newPhone); } } // 代理类:维护可变的Customer引用 public sealed class CustomerReference { private Customer _currentCustomer; public CustomerReference(Customer initialCustomer) { _currentCustomer = initialCustomer; } // 对外暴露当前有效的Customer实例 public Customer Current => _currentCustomer; // 仅允许更新同ID的Customer实例 public void UpdateCustomer(Customer newCustomer) { if (newCustomer.Id != _currentCustomer.Id) throw new ArgumentException("只能更新同一客户的实例"); _currentCustomer = newCustomer; } } // Order类:持有代理而非直接持有Customer public class Order { private readonly CustomerReference _customerRef; public Order(CustomerReference customerRef) { _customerRef = customerRef; } public string GetCustomerPhone() { // 每次获取都从代理拿最新实例 return _customerRef.Current.PhoneNumber; } } // Invoice类:复用代理逻辑,无需额外修改 public class Invoice { private readonly CustomerReference _customerRef; public Invoice(CustomerReference customerRef) { _customerRef = customerRef; } public string GetCustomerContact() { return _customerRef.Current.PhoneNumber; } }
使用方式
// 初始化客户及代理 var initialCustomer = new Customer("C001", "13800138000"); var customerRef = new CustomerReference(initialCustomer); // 创建关联业务对象 var order = new Order(customerRef); var invoice = new Invoice(customerRef); // 更新客户手机号:创建新实例+更新代理 var updatedCustomer = initialCustomer.WithPhoneNumber("13900139000"); customerRef.UpdateCustomer(updatedCustomer); // Order和Invoice自动获取最新手机号 Console.WriteLine(order.GetCustomerPhone()); // 输出13900139000 Console.WriteLine(invoice.GetCustomerContact()); // 输出13900139000
这个方案的核心优势:新增任何关联Customer的业务类时,只要持有CustomerReference就自动支持同步更新,完全不会出现遗漏事件注册的问题,同时契合不可变对象的设计原则。
二、若坚持使用观察者模式:编译器/启动阶段的检测方案
如果必须基于观察者模式实现,可通过接口强制+自动注册+静态代码分析的组合,在编译或应用启动阶段检测遗漏的事件订阅。
1. 定义强制接口
让所有需要监听Customer变更的类必须实现ICustomerObserver接口:
public interface ICustomerObserver { void OnCustomerUpdated(Customer oldCustomer, Customer newCustomer); } // Order实现接口 public class Order : ICustomerObserver { private Customer _customer; public Order(Customer customer) { _customer = customer; } public void OnCustomerUpdated(Customer oldCustomer, Customer newCustomer) { if (_customer.Id == oldCustomer.Id) _customer = newCustomer; } }
2. 依赖注入自动注册
利用DI容器扫描所有实现ICustomerObserver的类型,自动完成事件注册(以.NET Autofac为例):
var builder = new ContainerBuilder(); // 注册所有ICustomerObserver实现类 builder.RegisterAssemblyTypes(Assembly.GetExecutingAssembly()) .Where(t => typeof(ICustomerObserver).IsAssignableFrom(t) && !t.IsInterface) .As<ICustomerObserver>(); var container = builder.Build(); // 批量注册事件 var observers = container.Resolve<IEnumerable<ICustomerObserver>>(); foreach (var observer in observers) { Customer.CustomerUpdated += observer.OnCustomerUpdated; }
新增业务类时,只要实现ICustomerObserver,DI容器就会自动完成事件注册,避免手动修改事件处理器的遗漏。
3. 静态代码分析/单元测试检测
编写自定义静态代码分析规则(如C# Roslyn分析器),或者单元测试,检测:
- 所有持有Customer引用的类,是否都实现了
ICustomerObserver接口 - Customer的事件注册逻辑是否包含了所有
ICustomerObserver实现类
比如单元测试示例(反射实现):
[Test] public void AllCustomerDependentTypesImplementObserver() { var assembly = Assembly.GetExecutingAssembly(); // 找到所有持有Customer字段/属性的类 var customerDependentTypes = assembly.GetTypes() .Where(t => t.GetFields(BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public) .Any(f => f.FieldType == typeof(Customer)) || t.GetProperties(BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public) .Any(p => p.PropertyType == typeof(Customer))); // 检查每个类是否实现了ICustomerObserver var missingObserverTypes = customerDependentTypes .Where(t => !typeof(ICustomerObserver).IsAssignableFrom(t)) .ToList(); Assert.IsEmpty(missingObserverTypes, $"以下类持有Customer引用但未实现ICustomerObserver:{string.Join(", ", missingObserverTypes.Select(t => t.Name))}"); }
这个测试会在运行时检测遗漏的类,若有未实现接口的情况直接报错。
内容的提问来源于stack exchange,提问作者Viktor Plaček
相关产品推荐
相关产品推荐

