Value objects

Value object pattern is not unique to Domain-Driven Design, but it probably became most popular within the DDD community. It probably happened due to such characteristics of value objects as expressiveness and strong encapsulation. Fundamentally, value objects allow declaring entity properties with explicit types that use Ubiquitous Language. Besides, such objects can explicitly define how they can be created and what operations can be performed within and between them. It is a perfect example of making implicit explicit.

Let's look closer to what value object is, by creating one in our code. Before, we were taking the ownerId parameter in the entity constructor, and checking it to have a non-default GUID. What we want here is user id, since we know that the ad owner is one of our users, because people need to be registered in the system before creating classified ads. It means that we can embrace the type system and make implicit more explicit by using a new type called UserId, instead of using Guid.

Let's create a new class in the Marketplace.Domain project and call it UserId. The initial code for this class would be like this:

using System;

namespace Marketplace.Domain
{
public class UserId
{
private readonly Guid _value;

public UserId(Guid value)
{
if (value == default)
throw new ArgumentNullException(
nameof(value), "User id cannot be empty");

_value = value;
}
}
}

As you can see, we moved the assertion that the identity value is not an empty GUID, to the UserId constructor. It means that we can change our entity constructor to this:

public class ClassifiedAd
{
public Guid Id { get; }

private UserId _ownerId;

public ClassifiedAd(Guid id, UserId ownerId)
{
if (id == default)
throw new ArgumentException(
"Identity must be specified", nameof(id));

Id = id;
_ownerId = ownerId;
}

// rest of the code skipped
}

Our entity has no check for the owner id since by receiving the argument of type UserId we guarantee that the value is valid. Of course, we do not check here if the supplied GUID points to a valid user, but this was not our intention here, at least for now.

But we still have one more check for the argument validity in the entity constructor. Let's make the entity id type a value object too by adding a ClassifiedAdId class with the following code:

using System;

namespace Marketplace.Domain
{
public class ClassifiedAdId
{
private readonly Guid _value;

public ClassifiedAdId(Guid value)
{
if (value == default)
throw new ArgumentNullException(
nameof(value),
"Classified Ad id cannot be empty");

_value = value;
}

}
}

Now our constructor has no checks at all, and it still makes a valid entity:

public class ClassifiedAd
{
public ClassifiedAdId Id { get; }

private UserId _ownerId;

public ClassifiedAd(ClassifiedAdId id, UserId ownerId)
{
Id = id;
_ownerId = ownerId;
}

// rest of the code skipped
}

As we move to the application layer, where our entity would be constructed, we could imagine that calls to the constructor would look like this (assuming that id and ownerId are of type Guid):

var classifiedAd = new ClassifiedAd(new ClassifiedAdId(id), new UserId(ownerId));

The code above clearly says that we are sending the classified ad id first and the owner id second to the entity constructor. When we used Guid as the type for both parameters, if we accidentally change the order of parameters, our application would still compile but of course, our entities will be constructed incorrectly, and the whole system would break somewhere deep down the execution pipeline. Strongly typed parameters of value object types enforce the compiler to engage type checking and if we messed up arguments, the code won't compile.

But value objects aren't just wrapper types around primitive types. As we learned before, entities are considered equal if their identities are the same. Value objects are different since their equality is establishing by value, hence the pattern name. A classical example of a value object is money. If we take two €5 banknotes, they represent two different entities since they are in fact two distinctly different objects and even have unique numbers printed on them. But for payment, both are entirely identical since they have the same value of €5.

But how do we represent it in code? Let's create the Money class and give it a try.

namespace Marketplace.Domain
{
public class Money
{
public decimal Amount { get; }

public Money(decimal amount) Amount = amount;
}
}

Now, let's write a simple test to check if two objects of type Money are equal if the amount is equal:

using Marketplace.Domain;
using Xunit;

namespace Marketplace.Tests
{
public class MoneyTest
{
[Fact]
public void Money_objects_with_the_same_amount_should_be_equal()
{
var firstAmount = new Money(5);
var secondAmount = new Money(5);

Assert.Equal(firstAmount, secondAmount);
}
}
}

Of course, this test fails because a class instance is a reference object and two instances of the same class are different objects, no matter what their properties and fields contain. We can conclude that neither the Money class nor our UserId and ClassifiedAdId classes can represent value objects.

To make the Money class closer to proper value object type, we need it to implement the IEquatable interface. The class instance will need to be compared with instances of the same type, so we need Money to implement IEquatable<Money>. If you add this interface to the class, in Rider, and in Visual Studio with Resharper, there will be a possibility to generate the necessary code automatically using the "Generate equality members" refactoring suggestion.

Generate equality members in Rider

Hence that if the "Overload equality operators" option is enabled, code for implicit equality operators will also be created. So, the code for our Money class will look like this:

using System;

namespace Marketplace.Domain
{
public class Money : IEquatable<Money>
{
public decimal Amount { get; }

public Money(decimal amount) => Amount = amount;

public bool Equals(Money other)
{
if (ReferenceEquals(null, other)) return false;
if (ReferenceEquals(this, other)) return true;
return Amount.Equals(other.Amount);
}

public override bool Equals(object obj)
{
if (ReferenceEquals(null, obj)) return false;
if (ReferenceEquals(this, obj)) return true;
if (obj.GetType() != this.GetType()) return false;
return Equals((Money) obj);
}

public override int GetHashCode() => Amount.GetHashCode();

public static bool operator ==(Money left, Money right) => Equals(left, right);

public static bool operator !=(Money left, Money right) => !Equals(left, right);
}
}

If we run the same test now, it will pass because when we call Assert.Equals(firstAmount, secondAmount), the code above will compare values of the _value field for both instances when these values are the same. Because we also created code for implicit equality operators, we can use comparisons like if (firstAmount == secondAmount) in our code.

Now, imagine we need all this code for each value object type we create. Yes, with some nice auto-magic from Resharper we can generate this code very quickly and then hide it in a region, which will always be collapsed. But if we decide to add one more attribute to the value object, we will need to reopen this region and add this new attribute in several places.

We can reduce the amount of boilerplate code and provide the ability for equality compare methods to be dynamic, by using a base class. There are at least two ways to create such a base class. One includes using reflections to discover all fields in the implementation type and use all of them for equality purposes. Another method involves creating an abstract method that needs to be overridden in each implementation to provide specific values that are used for equality. While the first method allows writing less code since all fields are automatically discovered and used, the second method allows to chose, which attributes will be used for equality.

[Todo: provide code or links]

In one of the next versions of C#, which might already be available when this book is published, the new feature will be introduced that is called record types. On a high level, record types will be similar to F# records. Using record types, declaration of value objects would become very short, and all boilerplate code for equality (and more) will be generated by the compiler.

For example, declaring the Money type above would be done in one line like this:

public struct Money(double amount);

Hence that here I used a struct, not a class. It provides real immutability because structs are value objects, unlike classes, which are reference objects. It means that when we assign a struct value to a new struct variable, we get a new struct with the same value, not a reference to the same object like it would have happened with classes.

Using the abstract base class in the Marketplace.Framework project, we can now refactor the Money class to this:

using Marketplace.Framework;

namespace Marketplace.Domain
{
public class Money : Value<Money>
{
public decimal Amount { get; }

public Money(decimal amount) => Amount = amount;
}
}

As you can see, all the boilerplate code is now moved to the base class, and we got back to essentials. The test, however, still passes because of the proper equality implementation in the base class.

So far we only had straightforward rules in value objects, but when we work with money, we should be adding one useful check. Rarely, if we talk about money, we mean a negative amount. Yes, such amounts exist in accounting, but we are not building an accounting system. In our domain, classified ads need to have a price, and the price cannot be negative, as our domain expert explained. So, we can represent this rule in a new value object:

using System;

namespace Marketplace.Domain
{
public class Price : Money
{
public Price(decimal amount) : base(amount)
{
if (amount < 0)
throw new ArgumentException(
"Price cannot be negative",
nameof(amount));
}
}
}

Thus, despite our base Money class still allows its amount to be negative or zero, the price will always be positive and by this, valid in our domain.

Speaking about immutability, we must ensure that there are no methods that our value objects expose, which allow changing field values inside these objects. If we want to do some operation on a value object instance, it needs to produce a new instance of the same type but with a new value. By doing this, we ensure that the original object will retain its value.

Let's look at the Money example and add some useful operations to it, keeping immutability in mind:

using Marketplace.Framework;

namespace Marketplace.Domain
{
public class Money : Value<Money>
{
public decimal Amount { get; }

public Money(decimal amount) => Amount = amount;

public Money Add(Money summand) =>
new Money(Amount + summand.Amount);

public Money Subtract(Money subtrahend) =>
new Money(Amount - subtrahend.Amount);

public static Money operator +(
Money summand1, Money summand2) => summand1.Add(summand2);

public static Money operator -(
Money minuend, Money subtrahend) =>
minuend.Subtract(subtrahend);
}
}

If we a sum of €1 coin and two €2 coins, the total value is €5. If we compare it with a banknote of €5, its value is the same. Since we aren't interested in shape, size, and weight of those monetary instruments and only interested in value, we can conclude that those two have equal value.  Our new Money class above lets us to express this statement in the test code, which will be green when we run it:

[Fact]
public void Sum_of_money_gives_full_amount()
{
var coin1 = new Money(1);
var coin2 = new Money(2);
var coin3 = new Money(2);

var banknote = new Money(5);

Assert.Equal(banknote, coin1 + coin2 + coin3);
}

Now, we can finally rewrite our identity classes to proper value object implementations:

public class ClassifiedAdId : Value<ClassifiedAdId>
{
private readonly Guid _value;

public ClassifiedAdId(Guid value) => _value = value;
}
public class UserId : Value<UserId>
{
private readonly Guid _value;

public UserId(Guid value) => _value = value;
}

Now, let's have a more in-depth look at more advanced ways to instantiate value objects and entities.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.118.7.102