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

为何创建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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.14 07:46:57