遵循DDD实践,值对象的实现是否应密封?以Money类为例
领域驱动设计中值对象是否应该密封?
这是个非常贴合DDD实践细节的好问题!直接给结论:是的,你应该把Money这类值对象的具体实现类设置为sealed(密封)。下面从DDD设计原则和代码实践两个层面给你拆解原因:
一、贴合值对象的核心特性
DDD里的值对象是“值优先”的无身份对象,它的唯一标识就是自身的所有属性值,而且行为和属性定义是完整、不可变的。如果允许值对象被继承:
- 子类可能新增属性或修改行为,破坏值对象的相等性逻辑(比如两个
Money子类实例,基础属性完全一致,但子类新增的属性不同,会被判断为不相等,这和业务上“同值即相等”的预期冲突); - 值对象本身不需要多态扩展,业务上的不同值类型(比如
Money和Address)应该是独立的类,而非继承关系。密封类能从设计上杜绝这种不必要的扩展。
二、保护值对象的创建与约束逻辑
看你的Money代码,构造函数是private的——这显然是为了通过工厂方法(比如后续会加的Create方法)严格控制实例的创建,确保只有合法的Money能被生成。如果不密封:
- 即使子类无法访问父类的私有构造,也可能有人试图通过奇怪的方式绕开你的约束;
- 密封类能彻底锁死这个类的继承可能性,让你的创建逻辑完全可控。
三、优化相等性判断的准确性与性能
通常ValueObject<T>的相等性判断会依赖GetType()来确保类型一致(比如other.GetType() == GetType())。如果Money是密封类:
- 可以确保不会有子类混入相等性判断,避免出现“看似同值但类型不同(子类)”的误判;
- C#的JIT编译器会对密封类的类型检查做优化,提升相等性判断的性能。
补充代码示例(添加sealed)
public sealed class Money : ValueObject<Money> { private Money() { } private Money(decimal value, string currency) { Requires.NotEmpty(currency, nameof(currency)); Requires.That(value >= 0, $"{nameof(value)} must be greater or equals to 0."); Value = value; Currency = currency; } // 典型的工厂方法,控制实例创建 public static Money Create(decimal value, string currency) => new Money(value, currency); public decimal Value { get; } public string Currency { get; } // 实现ValueObject的核心相等性逻辑 protected override bool EqualsCore(Money other) { return Value == other.Value && Currency == other.Currency; } protected override int GetHashCodeCore() { return HashCode.Combine(Value, Currency); } }
总的来说,密封值对象是DDD实践里的一个最佳实践,它能帮你维护值对象的纯洁性,避免后续出现意料之外的设计漏洞。
内容的提问来源于stack exchange,提问作者Dmytro Zhluktenko
相关产品推荐
相关产品推荐

