Value of Value Objects

Take advantage of Value Object characteristics to make business rules more explicit, code more reusable, and applications more consistent.

Let’s say we are working on a system dealing with orders and package deliveries to customers’ addresses. Here is a simple example of what might be part of the Order Aggregate Root with the ChangeDeliveryAddress command method. The method handles the validation of input parameters, checks some business rules, and produces the DeliveryAddressChanged Domain Event.

public sealed class Order : AggregateRoot
{
    public override OrderId { get; protected set; }
    private string DeliveryStreet { get; set; }
    private string DeliveryCity { get; set; }
    private Country DeliveryCountry { get; set; }
    public Order(IEnumerable events) : base(events) { }
    public void ChangeDeliveryAddress(
        string newDeliveryStreet,
        string newDeliveryCity,
        Country newDeliveryCountry)
    {
        if (string.IsNullOrWhiteSpace(newDeliveryStreet)
            || string.IsNullOrWhiteSpace(newDeliveryCity)
                throw new IncompleteAddressException();
        if (DeliveryStreet == newDeliveryStreet
            && DeliveryCity == newDeliveryCity
            && DeliveryCountry == newDeliveryCountry)
                throw new TheSameDeliveryAddressAlreadySetException();
        if (newDeliveryCountry == Country.BosniaAndHerzegovina)
            throw new UnsupportedDeliveryAddressException();
        Apply(new DeliveryAddressChanged(
            OrderId,
            newDeliveryStreet,
            newDeliveryCity,
            newDeliveryCountry));
    }
    private void On(DeliveryAddressChanged @event)
    {
        DeliveryStreet = @event.Street;
        DeliveryCity = @event.City;
        DeliveryCountry = @event.Country;
    }
}
Copy

Now, let’s consider introducing a Value Object where appropriate. In this context, Street, City, and Country do not make sense on their own and together they are just properties of DeliveryAddress. So, grouping Street, City, and Country into Address Value Object is the obvious choice. If one of the properties changes, the address changes as a whole, and in that case, DeliveryAddress should be replaced with a completely new instance of Address.

public sealed class Address : ValueObject
{
    public string Street { get; }
    public string City { get; }
    public Country Country { get; }
    public Address(string street, string city, Country country)
    {
        if (string.IsNullOrWhiteSpace(street)
            || string.IsNullOrWhiteSpace(city)
                throw new IncompleteAddressException();
        Street = street;
        City = city;
        Country = country;
    }
    public override bool Equals(object obj)
    {
        if (obj is Address address)
            return Street == address.Street
                && City == address.City
                && Country == address.Country;
        return false;
    }
    public bool IsInCountry(Country country)
    {
        return Country == country;
    }
    public string ToAddressLine()
    {
        return $”{Street}, {City.ToUpper()}, {Country.Name()}”;
    }
}
Copy

Let’s go through the characteristics of the newly created Address Value Object. Ensuring individual properties can not be set separately, only through the constructor, makes an instance of Address practically immutable. On constructing a new instance of Address, validation of input parameters is handled, which essentially makes an instance of Address always valid. By defining the Equals method we made two instances of Address comparable. Two addresses are equal only if all their properties are equal. Let’s see what the Order looks like now.

public sealed class Order : AggregateRoot
{
    public override OrderId { get; protected set; }
    private Address DeliveryAddress { get; set; }
    public Order(IEnumerable events) : base(events) { }
    public void ChangeDeliveryAddress(Address newDeliveryAddress)
    {
        if (DeliveryAddress.Equals(newDeliveryAddress))
            throw new TheSameDeliveryAddressAlreadySetException();
        if (newDeliveryAddress.IsInCountry(Country.BosniaAndHerzegovina))
            throw new UnsupportedDeliveryAddressException();
        Apply(new DeliveryAddressChanged(OrderId, newDeliveryAddress));
    }
    private void On(DeliveryAddressChanged @event)
    {
        DeliveryAddress = @event.Address;
    }
}
Copy

The first noticeable difference is that the Aggregate Root has less code and it is more readable. What’s more important, invalid data can never reach command methods. Validations of input parameters are done in the Value Object itself, so no need to repeat the same code in command methods or other parts of the application. This way, command methods check business requirements only, which makes business rules easier to recognize, more clear, and more explicit. Defining and using equality and behavioral methods of Value Objects may significantly contribute to the consistency of an application.

In this small piece of code, the effects of introducing a Value Object may not seem revolutionary. But, on a larger scale, using the concept of Value Objects can enhance the maintainability and quality of the system noticeably.