Do not be afraid to (over-)use Value Objects

From the things I've learned from DDD

ยท

5 min read

Learning Domain-Driven Design allows one to precisely model a domain-specific problem into fixed-purpose software. When building specialized software for a particular domain or business model, DDD helps make sure that what the business users expect the software to do, in terms of core business, is what the software actually does, by bringing closer and aligning developers with business users.

But, Domain-Driven Design is not the best approach for everything, for example it might not be the best approach for building generalized or highly abstract solutions that need to be able to adapt to multiple domains (for example, workflow engines, although one could argue that there is a domain there as well). Regardless of that, there are a lot of useful things to learn from DDD.

One of them is Value Objects, a tactical design pattern.

Martin Fowler has an article on his bliki about Value Objects here, but basically, all they are is an encapsulation of related information that enforces business invariants or rules, such that the sum data (viewed as a unit) is always kept in a consistent and valid state and, thanks to OOP, its behavior can also be modeled and customized as well. For example, you can override what it means to compare two similar VOs, how that works under the hood in order to simplify working with VOs, or you can customize what kind of operands even make sense for certain VOs.

One interesting example is modelling money as VOs.

The question whether to use float, double, decimal or int to store money in computer systems seem to be never-ending. Going past the fact that decimal is undoubtedly the best option, besides perhaps storying the base and the decimal values as separate ints and implementing basic arithmetic operations manually for the pair, instead of actually storying money as a decimal field and the currency as a separate field (could be a string an enum or another VO, it doesn't matter), try modelling money as a Value Object to get some important benefits:


// note that this is just one possible implementation of a VO base class,
// others exist as well, including generic versions, but I like this one more.

public abstract class ValueObject
{
    protected abstract IEnumerable<object> GetEqualityComponents();

    public override bool Equals(object obj)
    {
        if (obj == null)
            return false;

        if (GetType() != obj.GetType())
            return false;

        var valueObject = (ValueObject)obj;

        return GetEqualityComponents().SequenceEqual(valueObject.GetEqualityComponents());
    }

    public override int GetHashCode()
    {
        return GetEqualityComponents()
            .Aggregate(1, (current, obj) =>
            {
                unchecked
                {
                    return current * 23 + (obj?.GetHashCode() ?? 0);
                }
            });
    }

    public static bool operator ==(ValueObject a, ValueObject b)
    {
        if (ReferenceEquals(a, null) && ReferenceEquals(b, null))
            return true;

        if (ReferenceEquals(a, null) || ReferenceEquals(b, null))
            return false;

        return a.Equals(b);
    }

    public static bool operator !=(ValueObject a, ValueObject b)
    {
        return !(a == b);
    }
}

public class Money : ValueObject
{
    public Money(decimal amount, string currency)
    {
        Value = amount;
        // below is just an example of what maintaining business invariants
        // could look like, through helpers that throw custom exceptions:
        Currency = Guard.Against.NotIn(AllowedCurrenciesList, nameof(currency));
    }

    public decimal Value { get; private set; }
    public string Currency { get; private set; }

    // THIS IS THE ONLY METHOD YOU NEED TO OVERRIDE FOR EQUALITY COMPARISON:
    protected override IEnumerable<object> GetEqualityComponents()
    {
        yield return Math.Round(Value, 2); // or how many significant decimal digits you require
        yield return Currency.ToUpper();
    }
}

public class Invoice
{
    public string Number { get; private set; }
    public Money Total { get; private set; }
}

These benefits are:

  • Immutability (once created, cannot be mutated by accident)
  • Custom behavior and control (pick equality members, data conversion, control over equality precision)
  • Data consistency guarantees (due to immutability and consolidated validation logic)
  • Code reuse (you can now reuse Money everywhere you need in your system with the same guarantees)

And here's a cool thing you can get with EF Core if you use VOs as owned entity types:

The above Invoice will be mapped to the database to a single table, with Total, a Money instance, flattened down to fields inside the Invoice table, like this:

{
    "Number": "12345",
    "Total_Value": 99.99,
    "Total_Currency": "USD"
}

Notice the Total field that got flattened with its property members in a sort of neat fashion.

So, don't be afraid to use Value Objects, especially if you have some pieces of related data that should live together, be validated together, etc. But, as a whole, this collection of related data is not an entity, it does not have a unique ID!

For example you can have dozens of instances of Money all with the same currency and value, but they are not the same object, they just have the same value like how my $20 are equally as valuable as your $20.

Addendum

Some people have asked me why I don't use C# Records since "that's what they're basically made for", to behave like immutable values, but that is a partial misunderstanding since there are more drawbacks to using C# Records now than I would care to have.

Records are very new right now and perhaps in the future they will evolve to where I would prefer to use them instead of base classes, but for now, I find this article to offer a very good comparison between these two options.

EDIT

I might need to take another look at records as value objects with C# 10. There could be things I misunderstood or took for granted by reading other people's articles, but also C# 10 introduces record class and record struct, which is very exciting.

ย