1

What would be the reason to create different subtypes of Weight? I see our devs created NetWeight and GrossWeight subtypes as Value Objects. They both have the same implementation. Is there any value in it? Why not using Weight value type for both scenarios? I feel its totally wrong, would like to hear an expert's opinion on this...If you feel its wrong, how would you explain why its wrong?

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 
}
DmitriBodiu
  • 1,120
  • 1
  • 11
  • 22

1 Answers1

2

What would be the reason to create different subtypes of Weight?

If GrossWeight and NetWeight are different distinct ideas in your domain, if it would be an error to substitute a gross weight for a net weight, and if you are using a language that will flag incompatible types, then establishing GrossWeight and NetWeight as distinct types allows you to use the language's own type checker to flag (and thereby eliminate) certainly classes of errors that might otherwise escape into production.

The fact that the two different kinds of weights currently have equivalent implementations is essentially an implementation accident. In languages that support it, you could consider having those two types inherit their implementations from some common ancestor.

Many models will treat identifiers using a similar pattern -- even though at the implementation level there are no differences, it can be useful to distinguish a CustomerId type from an OrderId type to ensure that you don't corrupt your data set by unintentionally substituting one for the other.

Another place where you will see this approach is in models that distinguish untrusted inputs from trusted data. Sure, the underlying primitive representation is "just bytes", but it allows you to leverage the type system to distinguish data that has passed through a validating checkpoint from data which has not.

VoiceOfUnreason
  • 52,766
  • 5
  • 49
  • 91