为何创建Weight不同子类型?NetWeight与GrossWeight重复实现合理性探讨
关于NetWeight与GrossWeight作为独立值对象的设计分析
首先明确你的核心疑问:
我司开发人员将NetWeight和GrossWeight实现为Value Object,二者代码实现完全一致。请问创建Weight不同子类型的原因是什么?此举是否具备价值?为何不统一使用Weight值类型覆盖两种场景?我认为该做法存在问题,恳请专家给出专业观点;若确实不合理,请说明原因。
附上你提供的NetWeight实现代码:
public struct NetWeight : IEquatable<NetWeight> { private const string DefaultMeasurementUnit = "kg"; public double Value { get; } public string MeasurementUnit { get; } public NetWeight(double value, string measurementUnit) { if (value < 0) throw new BusinessRuleValidationException("NetWeight value can't be negative"); if (string.IsNullOrWhiteSpace(measurementUnit)) throw new BusinessRuleValidationException("NetWeight measurement unit can't be null or whitespace"); Value = value; MeasurementUnit = measurementUnit.Trim(); } public override string ToString() { return $"{Value}{MeasurementUnit}"; } public static NetWeight operator +(NetWeight left, NetWeight right) { if (left.MeasurementUnit != right.MeasurementUnit) throw new ArgumentException("Measurement units are not the same"); return new NetWeight(left.Value + right.Value, left.MeasurementUnit); } public static NetWeight operator -(NetWeight left, NetWeight right) { if (left.MeasurementUnit != right.MeasurementUnit) throw new ArgumentException("Measurement units are not the same"); return new NetWeight(left.Value - right.Value, left.MeasurementUnit); } public static NetWeight operator *(NetWeight left, Quantity right) { if (left.MeasurementUnit != right.MeasurementUnit) throw new ArgumentException("Measurement units are not the same"); return new NetWeight(left.Value * right.Value, left.MeasurementUnit); } public static NetWeight operator *(Quantity left, NetWeight right) { if (left.MeasurementUnit != right.MeasurementUnit) throw new ArgumentException("Measurement units are not the same"); return new NetWeight(left.Value * right.Value, left.MeasurementUnit); } // TODO: come up with a refactoring that prevents the use of primitive types public static NetWeight operator *(NetWeight left, int right) { return new NetWeight(left.Value * right, left.MeasurementUnit); } #region IEquatable public override bool Equals(object obj) { return obj is NetWeight weight && Equals(weight); } public bool Equals(NetWeight other) { return Value == other.Value && MeasurementUnit == other.MeasurementUnit; } public override int GetHashCode() { return HashCode.Combine(Value, MeasurementUnit); } public static bool operator ==(NetWeight left, NetWeight right) { return left.Equals(right); } public static bool operator !=(NetWeight left, NetWeight right) { return !(left == right); } #endregion }
一、开发人员创建独立子类型的核心原因
从领域驱动设计(DDD)和类型安全的角度来看,这个设计的出发点通常是这几点:
- 强业务语义表达:净重(NetWeight)和毛重(GrossWeight)是完全不同的业务概念——毛重包含产品包装、托盘等附加重量,净重是产品本身的实际重量。用独立类型可以把这种业务差异固化到代码的类型系统中,而不是靠变量名或注释来区分。
- 编译期错误预防:最直接的好处是类型安全——编译器会阻止你把GrossWeight的值错误地赋值给需要NetWeight的字段/方法参数。比如你不可能写
NetWeight net = new GrossWeight(10, "kg");,这种错误在编译阶段就会被拦截,比运行时校验或人工检查可靠得多。 - 未来扩展的预留:当前两个类型代码完全一致,但业务需求是变化的。比如未来可能要求NetWeight必须符合某个行业的精度标准,或者GrossWeight必须大于对应的NetWeight(因为毛重=净重+包装重),独立类型可以让你在不影响另一个类型的前提下添加专属规则。
二、这个做法是否具备价值?
答案是如果基于业务语义和类型安全的目标,这个设计是有明确价值的:
- 它让代码更贴合业务语言,其他开发人员看到
NetWeight就能立刻明白它的含义,不需要额外上下文。 - 彻底避免了“把毛重当净重使用”这类低级但致命的业务bug,这类bug如果靠运行时校验,不仅性能有损耗,还可能在上线后才暴露。
- 符合DDD中“用领域对象表达业务概念”的核心原则,让代码的维护性和可读性更高。
当然,它也有明显的缺点:当前的实现存在大量重复代码,这会增加维护成本——如果要修改通用逻辑(比如调整重量的校验规则),需要同时修改两个类型。但这个问题是可以通过重构解决的,不是设计本身的问题。
三、为什么不统一使用Weight值类型?
如果统一用一个Weight类型,会直接丢失业务语义信息,带来以下问题:
- 类型不安全:你可以随意把“代表毛重的Weight”赋值给“需要净重的变量”,编译器不会报错,只能靠人工检查或运行时校验,非常容易引入bug。
- 业务意图模糊:看到
Weight weight这个变量,你无法直接判断它是净重还是毛重,必须依赖变量名(比如netWeight、grossWeight)或注释,一旦变量名写错,就会造成理解混乱。 - 规则扩散:如果后续需要针对两种重量添加不同的业务规则,你只能在
Weight类型中添加大量条件判断(比如if (isNetWeight) ... else ...),导致类型逻辑变得臃肿,违反单一职责原则。
四、优化建议:保留类型安全,消除代码重复
如果你觉得当前的重复代码难以维护,可以通过以下方式重构:
方案1:提取抽象基类
创建一个抽象的Weight基类,把通用逻辑(校验、运算符重载、相等性判断等)放在基类中,NetWeight和GrossWeight只需要继承并复用基类逻辑:
public abstract class Weight : IEquatable<Weight> { private const string DefaultMeasurementUnit = "kg"; public double Value { get; } public string MeasurementUnit { get; } protected Weight(double value, string measurementUnit) { if (value < 0) throw new BusinessRuleValidationException("Weight value can't be negative"); if (string.IsNullOrWhiteSpace(measurementUnit)) throw new BusinessRuleValidationException("Weight measurement unit can't be null or whitespace"); Value = value; MeasurementUnit = measurementUnit.Trim(); } public override string ToString() => $"{Value}{MeasurementUnit}"; // 其他通用方法:相等性判断、GetHashCode等 public bool Equals(Weight other) => Value == other.Value && MeasurementUnit == other.MeasurementUnit; public override bool Equals(object obj) => obj is Weight weight && Equals(weight); public override int GetHashCode() => HashCode.Combine(Value, MeasurementUnit); } public struct NetWeight : Weight { public NetWeight(double value, string measurementUnit) : base(value, measurementUnit) { } // 可以添加NetWeight专属的运算符或规则 public static NetWeight operator +(NetWeight left, NetWeight right) { if (left.MeasurementUnit != right.MeasurementUnit) throw new ArgumentException("Measurement units are not the same"); return new NetWeight(left.Value + right.Value, left.MeasurementUnit); } } public struct GrossWeight : Weight { public GrossWeight(double value, string measurementUnit) : base(value, measurementUnit) { } // 可以添加GrossWeight专属的运算符或规则 public static GrossWeight operator +(GrossWeight left, GrossWeight right) { if (left.MeasurementUnit != right.MeasurementUnit) throw new ArgumentException("Measurement units are not the same"); return new GrossWeight(left.Value + right.Value, left.MeasurementUnit); } }
方案2:使用泛型标记
创建一个泛型Weight<TMarker>类型,用空的标记接口区分不同的重量类型,这样既复用所有逻辑,又保留类型安全:
// 空标记接口,仅用于区分类型 public interface INetWeightMarker { } public interface IGrossWeightMarker { } public struct Weight<TMarker> : IEquatable<Weight<TMarker>> where TMarker : class { private const string DefaultMeasurementUnit = "kg"; public double Value { get; } public string MeasurementUnit { get; } public Weight(double value, string measurementUnit) { if (value < 0) throw new BusinessRuleValidationException("Weight value can't be negative"); if (string.IsNullOrWhiteSpace(measurementUnit)) throw new BusinessRuleValidationException("Weight measurement unit can't be null or whitespace"); Value = value; MeasurementUnit = measurementUnit.Trim(); } public override string ToString() => $"{Value}{MeasurementUnit}"; public static Weight<TMarker> operator +(Weight<TMarker> left, Weight<TMarker> right) { if (left.MeasurementUnit != right.MeasurementUnit) throw new ArgumentException("Measurement units are not the same"); return new Weight<TMarker>(left.Value + right.Value, left.MeasurementUnit); } // 其他运算符、相等性判断等逻辑... public bool Equals(Weight<TMarker> other) => Value == other.Value && MeasurementUnit == other.MeasurementUnit; public override bool Equals(object obj) => obj is Weight<TMarker> weight && Equals(weight); public override int GetHashCode() => HashCode.Combine(Value, MeasurementUnit); } // 使用示例 var netWeight = new Weight<INetWeightMarker>(10, "kg"); var grossWeight = new Weight<IGrossWeightMarker>(12, "kg"); // 编译错误:无法将Weight<IGrossWeightMarker>转换为Weight<INetWeightMarker> // NetWeight wrong = grossWeight;
这两种方案都能保留类型安全的优势,同时彻底消除代码重复。
内容的提问来源于stack exchange,提问作者DmitriBodiu
相关产品推荐
相关产品推荐

