106  
csharp
Поиск  
Always will be ready notify the world about expectations as easy as possible: job change page
Dec 23, 2023

Domain-Driven Design: Understanding value objects

Domain-Driven Design: Understanding value objects
Автор:
Источник:
Просмотров:
6211

A guide to implementing value objects — Domain-Driven Design’s most powerful, yet least understood and utilized building block

Value Object
Generated using DALL-E 3

Value Objects are one of the most powerful building blocks in Domain-Driven Design for creating rich models, however programmers often shy away from using them. As Developers, we are generally much more familiar using Entities; this can create a barrier to really understanding Value Objects and when to use them.

When starting to use Domain-Driven Design, our default position is usually to use Entities to model different sections of our domain. This is actually the wrong way of thinking:

Favour Value Objects when modelling your domain, fall back to Entities if you must.

This article covers what Value Objects are and when to use them. We will also look at how Value Objects can be implemented and a few different examples where they shine. The example code uses C#, however the principles used can easily be applied to any programming language.

All of the principles used in the article are demonstrated in an example application in my GitHub here.

• • •

Entities vs Value Objects

Entities and Value Objects are the 2 fundamental building blocks of Domain-Driven Design. They are composed into Aggregates to model our domains. Before we get started, let’s do a quick recap of the difference between an Entity and a Value Object.

The primary difference between Entities and Value Objects is how their instances can be compared to each other.

Entities

  • An Entity is defined by its Unique Identifier.
  • Entities are mutable.
  • Aggregate Roots are always an Entity.
  • Entities are used to represent ‘things’.

Value Objects

  • Value Objects do not have a Unique Identifier.
  • Value Objects are considered to be the same if all of their Properties have the same Value.
  • Value Objects are immutable.
  • Value Objects are used to describe ‘things’.

Entity or Value Object?

Quiz time… Let’s take a look at when each should be used.

Quiz 1: What Entities or Value Objects could be used to model People?

{
    "socialSecurityNumber": "100-00-XXXX",
    "name": {
        "first": "Joe",
        "last": "Bloggs"
    },
    "email": "joebloggs@example.com"
}
  • Person is an Entity. A Person is defined by their unique Social Security Number.
  • Name is a Value Object. 2 different people may share the same Name, that does not mean they are the same Person.
  • Email could either be a property on the Person Entity, or it could be a Value Object. Here I would suggest to use a Value Object; Emails generally have quite a few invariants (business rules) which need to be satisfied and wrapping them in a Value Object helps us encapsulate this logic.

Quiz 2: What Entities or Value Objects could be used to model Cars?

{
    "registrationNumber": "XX23 ABC",
    "make": {
        "manufacturer": "Ford",
        "model": "Fiesta"
    },
    "wheels": [
        {
            "partNumber": "x00001",
            "productionDate": "2020-10-01",
            "position": "FrontLeft"
        },
        {
            "partNumber": "x00002",
            "productionDate": "2020-10-04",
            "position": "FrontRight"
        },
        // ...
    ]
}
  • Car is an Entity. A Car is defined by its unique Registration Number.
  • Make is a Value Object. 2 different Cars may share the same Make, that does not mean they are the same Car.
  • Wheel is an Entity. Even if a different Car’s Wheel had exactly the same Properties, it could not be the same Wheel. Cars cannot share wheels!

• • •

Value Object Implementation

When implementing a Value Object base type, the most important aspect to consider is the Equality method. Remember, a Value Object is Equal to another Value Object if all of its Properties are Equal.

Value Object Option 1

The first option delegates the Equals and GetHashCode logic to the implementing class.

public abstract class ValueObject<T> where T : ValueObject<T>
{
    public override bool Equals(object obj)
    {
        var valueObject = obj as T;

        if(valueObject == null)
        {
            return false;
        }

        return ValueEquals(valueObject);
    }

    public override int GetHashCode()
    {
        return GetValueHashCode();
    }

    protected abstract bool ValueEquals(T other);
    protected abstract int GetValueHashCode();
}

public class Name : ValueObject<Name>
{
    private Name(string first, string last)
    {
        First = first;
        Last = last;
    }

    public static Name Create(string first, string last)
    {
        Guard.Against.NullOrEmpty(first, nameof(First));
        Guard.Against.NullOrEmpty(last, nameof(Last));
        return new Name(first, last);
    }

    public string First { get; private set; }
    public string Last { get; private set; }

    protected override bool ValueEquals(Name other)
    {
        return First == other.First
            && Last == other.Last;
    }
    
    protected override int GetValueHashCode()
    {
        unchecked
        {
            return First.GetHashCode() * 23
                + Last.GetHashCode();
        }
    }
}

With the first option we can see that the Name Value Object must implement the logic for the Equals and GetHashCode abstract methods required by base class. If we have lots of Value Object classes in our solution then this may create a lot of additional logic to maintain (and test).

Value Object Option 2

The second option implements the logic for the Equals and GetHashCode methods in the base class, and only requires the implementing type to provide the Property components for it to compare.

public abstract class ValueObject
{
    public override bool Equals(object obj)
    {
        var valueObject = obj as ValueObject;

        if (valueObject == null)
        {
            return false;
        }

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

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

    protected abstract IEnumerable<IComparable> GetEqualityComponents();
}

public class Name : ValueObject
{
    private Name(string first, string last)
    {
        First = first;
        Last = last;
    }

    public static Name Create(string first, string last)
    {
        Guard.Against.NullOrEmpty(first, nameof(First));
        Guard.Against.NullOrEmpty(last, nameof(Last));
        return new Name(first, last);
    }

    public string First { get; private set; }
    public string Last { get; private set; }

    protected override IEnumerable<IComparable> GetEqualityComponents()
    {
        yield return First;
        yield return Last;
    }
}

The second option ultimately ends up requiring less code to be created for your Value Object implementations.

The option you pick may depend on which programming language you use or which performs better. I personally like Option 2 because there’s less code to maintain.

• • •

Value Objects In-Action

Now let’s take a look at some typical scenarios where Value Objects work really well.

Email

In the previous section we encapsulated a Person’s First Name and Last Name into a Value Object. There are lots of cases where we may only need to wrap a single property in a Value Object, Email is a great example of this.

public class Email : ValueObject
{
    private Email(string email)
    {
        Value = email;
    }

    public static Email Create(string email)
    {
        email = (email ?? string.Empty).ToLower().Trim();
        Guard.Against.NullOrEmpty(email, "Email");
        try
        {
            _ = new MailAddress(email);
        }
        catch
        {
            throw new DomainException("Invalid Email");
        }
        return new Email(email);
    }

    public string Value { get; private set; }

    protected override IEnumerable<IComparable> GetEqualityComponents()
    {
        yield return Value;
    }
}

Here an Email is encapsulated in a single Value property, however our Value Object provides much more than that. Remember that one of the principles of Domain-Driven Design Aggregates is that they should ‘always be valid’. Here our Email Value Object ensures that it only accepts valid values with the following logic:

  • Trim any whitespace before or after the email string
  • Convert the email value to lowercase. This helps when comparing the value of emails in the future.
  • Validate whether the email is Null or Empty using a Guard class.
  • Validate the email is a valid email. Here I am using the C# MailAddress class which has all the validation I need, however you could use your own validation here.
  • Use of private setters to encapsulate and protect the state of the Value Object.

What we achieve is an Email Value Object which will always satisfy the invariants (business rules) for a users Email. This powerful Value Object could now be reused anywhere in our Domain that requires Emails to be captured.

Static Factory Methods

You might wonder why I have kept the constructor on my Value Objects private. I prefer to add static Factory Methods for instantiating instances of Value Objects (and Entities as well for that matter). This allows me to ensure that all of the validations that I need can be enforced, so that only Valid Value Objects can be created.

If you are using an ORM like NHibernate or Entity Framework, then it is likely that the private constructor or your classes will be used when data is read from your database. If we put the required validations in public constructors instead, then these validations would get re-run every time we read our data from the database… not ideal!

Money

Another common use-case for Value Objects is Money. Here our Money Value Object encapsulates an Amount and a 3 digit Currency code.

public class Money : ValueObject
{
    private Money(decimal amount, string currency)
    {
        Amount = amount;
        Currency = currency;
    }

    public static Money CreateDollars(decimal amount)
    {
        return Create(amount, "USD");
    }

    public static Money CreatePounds(decimal amount)
    {
        return Create(amount, "GBP");
    }

    public static Money Create(decimal amount, string currency)
    {
        currency = (currency ?? string.Empty).ToUpper().Trim();
        Guard.Against.LengthGreaterThan(currency, 3, nameof(Currency));
        Guard.Against.LengthLessThan(currency, 3, nameof(Currency));
        return new Money(amount, currency);
    }

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

    public Money Add(Money other)
    {
        if(Currency != other.Currency)
        {
            throw new DomainException("Different currencies cannot be added");
        }
        return new Money(Amount + other.Amount, Currency);
    }

    protected override IEnumerable<IComparable> GetEqualityComponents()
    {
        yield return Amount;
        yield return Currency;
    }
}

Our Money Value Object also demonstrates some additional features that can be useful for making richer Value Objects.

  1. Custom Factory Methods: Let’s assume we know that our application will regularly require Money to be in Dollars or Pounds. We can create specific Factory methods which make these easier to create by passing in the relevant Currency codes for us.
  2. Immutable Operations: Remember that one of the traits of Value Objects is that they are immutable. We can still create operations on our Value Objects but they must return a new instance. The Add method demonstrates this by first validating that the Money’s Currency is the same, and then adding the amounts together in a new Money instance if it is.

Battling Primitive Obsession

As I said at the start of the article, you should push yourselves to use Value Objects where you can, and fall back to Entities if you must. Another habit that Developers often get into when using Domain-Driven Design is to model Entities with tons of primitive properties on them e.g. String, Integers, Decimals, Dates. This is known as Primitive Obsession.

We have demonstrated how even simple properties can be abstracted away into powerful reusable Value Objects. You can combat Primitive Obsession by breaking down your Entities into smaller composable Value Objects instead. This will make your code much richer, easier to test and more reusable.

Let’s take our Person Entity from earlier, here is how it would look if we used only primitive properties to model everything.

public class Person : Entity
{
    private Person(string socialSecurityNumber, string firstName, string lastName, string email,
        string homeAddressLine1, string homeAddressLine2, string homeAddressCity, string homeAddressZipCode,
        string workAddressLine1, string workAddressLine2, string workAddressCity, string workAddressZipCode) : base(socialSecurityNumber)
    {
        FirstName = firstName;
        LastName = lastName;
        Email = email;
        HomeAddressLine1 = homeAddressLine1;
        HomeAddressLine2 = homeAddressLine2;
        HomeAddressCity = homeAddressCity;
        HomeAddressZipCode = homeAddressZipCode;
        WorkAddressLine1 = workAddressLine1;
        WorkAddressLine2 = workAddressLine2;
        WorkAddressCity = workAddressCity;
        WorkAddressZipCode = workAddressZipCode;
    }

    public static Person(string socialSecurityNumber, string firstName, string lastName, string email,
        string homeAddressLine1, string homeAddressLine2, string homeAddressCity, string homeAddressZipCode,
        string workAddressLine1, string workAddressLine2, string workAddressCity, string workAddressZipCode)
    {
        // lots of validation logic here
        // for every single property!
        
        return new Person(socialSecurityNumber, firstName, lastName, email,
          homeAddressLine1, homeAddressLine2, homeAddressCity, homeAddressZipCode,
          workAddressLine1, workAddressLine2, workAddressCity, workAddressZipCode);
    }

    public string FirstName { get; private set; }
    public string LastName { get; private set; }
    public string Email { get; private set; }
    public string HomeAddressLine1 { get; private set; }
    public string HomeAddressLine2 { get; private set; }
    public string HomeAddressCity { get; private set; }
    public string HomeAddressZipCode { get; private set; }
    public string WorkAddressLine1 { get; private set; }
    public string WorkAddressLine2 { get; private set; }
    public string WorkAddressCity { get; private set; }
    public string WorkAddressZipCode { get; private set; }
}

We end up with some fairly unwieldy code! We have loads of validations (I haven’t added them here as they will take up a lot of space…), huge constructors and lots of lines of code. Testing this code would also be a real pain, we would end up having lots of duplicate validations for things like the different Address properties.

If we come back to our principle of favouring Value Objects, then we end up with much cleaner code!

public class Person : Entity
{
    private Person(string socialSecurityNumber, Name name, Email email,
        Address homeAddress, Address workAddress) : base(socialSecurityNumber)
    {
        Name = name;
        Email = email;
        HomeAddress = homeAddress;
        WorkAddress = workAddress;
    }

    public static Person(string socialSecurityNumber, Name name, Email email,
        Address homeAddress, Address workAddress)
    {
        return new Person(socialSecurityNumber, name, email, homeAddress, workAddress);
    }

    public Name Name { get; private set; }
    public Email Email { get; private set; }
    public Address HomeAddress { get; private set; }
    public Address WorkAddress { get; private set; }
}

Since we know that our Value Objects already encapsulate any required business logic and validations, all of the validation logic can be removed from the Person Entity. We know that the required Value Objects will always be passed in with valid state.

By abstracting the Address into a Value Object, we can also reuse it for both the Home and Work Address.

• • •

Hopefully this article has demonstrated how powerful Value Objects can be for modelling complex domains. If you can push yourself to favour Value Objects over Entities and resist the temptation of Primitive Obsession, then you will end up with much richer, cleaner and more reusable code.

If you are getting started with Domain-Driven Design, then you may be interested in my previous article - Domain-Driven Design: A Walkthrough of Building an Aggregate

All of the principles used in this article are demonstrated in an example application in my GitHub below.

GitHub - matt-bentley/DDDMart: Sample eCommerce application using Domain-Driven Design

Похожее
May 6, 2023
Author: Miroslav Pillár
Handy guide how to enhance rating in Google and stand out from the crowd. You’ve deployed a promising website, but Google doesn’t show it in search results at all. I know how you feel. And if you don’t have many...
Nov 30, 2023
Author: Dev·edium
Keeping your C# applications safe and sound. Keeping app secrets safe is always tricky for developers. We want to work on the main parts of the app without getting distracted by secret-keeping. But, the app’s safety is very important. So,...
May 30
Author: Winds Of Change
Performance is paramount when developing web applications. A slow, unresponsive application results in poor user experience, losing users, and possibly business. For ASP.NET Core developers, there are many techniques and best practices to optimize application performance. Let’s explore some of...
Jun 14
Introduction to Async and Await In the world of JavaScript, asynchronous programming is a key concept for performing tasks that take some time to complete, like fetching data from an API or reading a file from the disk. It helps...
Написать сообщение
Тип
Почта
Имя
*Сообщение
RSS