Value Objects: when to create one?
In this post, we’ll discuss Value Objects. Specifically, when to introduce them into your code.
Value objects: when to create one?
A reader requested a code review for a Value Object and also asked an interesting question along the way. We will review the class shortly. For now, let’s look at the question first.
Here it is (paraphrased):
I wrote a small struct to restrict the user to only define decimals greater than zero. But now I’m wondering if such a small thing is worth the additional effort.
That’s a great question. It’s indeed hard at times to decide whether some concept in your code base is worth extracting into a separate Value Object. Sometimes, it makes more sense to just leave it as a primitive type and not bother with wrapping it into an additional class. Especially if you take into account the cost of doing so in languages like C#. Most languages require you to add quite a bit of noise when creating a Value Object, namely custom equality members and hash code calculation.
This can change if C# finally gets Record Types. With this feature, instead of doing this:
public struct Money : IEquatable<Money>
{
public int Amount { get; }
public CurrencySymbol Currency { get; }
public bool Equals(Money other)
{
return Amount == other.Amount && Currency == other.Currency;
}
public override bool Equals(object obj)
{
if (ReferenceEquals(null, obj))
return false;
return obj is Money && Equals((Money)obj);
}
public override int GetHashCode()
{
unchecked
{
return (Amount * 397) ^ (int)Currency;
}
}
}
you will be able to write just this single line:
public struct Money(int Amount, CurrencySymbol Currency);
and it would translate into an equivalent of the version above.
This will drastically reduce the cost of introducing Value Objects and will probably make it a no-brainer to create one in the vast majority of cases. For now, however, we need to work with what we have.
So where to draw this line? How to determine whether some domain concept is worth defining a separate class? And where to use plain primitives?
There’s no hard rule here and you need to consider every Value Object separately. But I can give you some general guidelines. There are 4 characteristics you need to look at.
The number and complexity of invariants. There are rules attached to every domain concept. The more rules there are, the better off you’d be wrapping it with its own class. For example, the positive decimal has only one such invariant: it has to be greater than zero. And this invariant itself is quite simple too. On the contrary, the concept of email has much more complex validation rules: we need to check if the candidate string matches a specific RegEx pattern, and is not longer than 255 characters.
The number of primitives it embodies. An email and a positive number can be represented with only one primitive type. However, there are plenty examples where you can’t do that and need at least two primitives to mimic the domain concept’s behavior. For example, this Money Value Object represents a number of physical coins and dollar notes. Each needs to be tracked separately, hence it’s impossible to represent this domain concept with just a single integer, you need a separate property for every such coin or note.
Overall project complexity. You need to also take into account the complexity of the project you are working on. A complex business line application warrants a different treatment comparing to a small command-line utility.
The number of duplications. Finally, you need to look at the number of duplications you would have should you not introduce a Value Object for some domain concept. As I mentioned earlier, each concept in the domain has its own set of invariants. If not gathered under an umbrella of a single class, those invariants will require you to perform validation checks each time you work with that concept represented as a primitive.
I would say that if the domain concept needs more than one primitive, then definitely introduce a separate Value Object for it. Make an exception only for trivial or throw-away applications. This practice would save you a lot of headaches in the future. Here’s a recent code review with an example of such a Value Object: Code review: Fabric class.
If the concept doesn’t require multiple primitive values and can be represented by just a single one, then you need to look at other characteristics. If the project is not too complex and the number of duplications is manageable, it might be OK not to introduce a Value Object even for complex concepts, such as Email. Otherwise, I would look at the complexity of invariants associated with the potential Value Object. Simple invariants are generally not worth being represented explicitly. But again, it all depends on the project.
So, would I introduce a Value Object for a positive number? Probably not. At least not in C#/Java and not in a typical enterprise application I usually work on. The single invariant here (being greater than zero) is quite simple. Even in the face of potential duplications (checking that the value is positive), it would probably make more sense to just leave it as a decimal most of the times.
But again, always mind the project context. The same domain concept may warrant a Value Object in one project and may not in another due to the other 2 characteristics I described earlier (overall project complexity and number of duplications). As I said, no hard rule here.
By the way, it’s interesting that Value Object is one of the notions that lie at the intersection of DDD and Functional Programming. Both pay close attention to it and I also talked about it in both of my corresponding Pluralsight courses: Domain-Driven Design in Practice and Applying Functional Principles in C#.
Alright, let’s now look at the code itself.
Here it is:
public struct DecimalGreaterThanZero : IEquatable<DecimalGreaterThanZero>
{
private readonly decimal _value;
public DecimalGreaterThanZero(decimal value)
{
if (CanCreate(value) == false)
throw new ArgumentException("Value must be greater than zero.", nameof(value));
_value = value;
}
public static bool CanCreate(decimal d)
{
return d > decimal.Zero;
}
private decimal ValueGreaterThanZero => _value == decimal.Zero ? 1 : _value;
public static implicit operator DecimalGreaterThanZero(decimal d)
{
return new DecimalGreaterThanZero(d);
}
public static implicit operator decimal(DecimalGreaterThanZero decimalGreaterThanZero)
{
return decimalGreaterThanZero.ValueGreaterThanZero;
}
public override bool Equals(object obj)
{
if (!(obj is DecimalGreaterThanZero))
return false;
return Equals((DecimalGreaterThanZero)obj);
}
public bool Equals(DecimalGreaterThanZero other)
{
return ValueGreaterThanZero == other.ValueGreaterThanZero;
}
public static bool operator ==(DecimalGreaterThanZero a, DecimalGreaterThanZero b)
{
return a.Equals(b);
}
public static bool operator !=(DecimalGreaterThanZero a, DecimalGreaterThanZero b)
{
return !a.Equals(b);
}
public override int GetHashCode()
{
return ValueGreaterThanZero.GetHashCode();
}
}
First of all, I personally like to use a base class for all Value Objects. It allows for gathering common equality logic into a single place which helps reduce the noise. It also allows you to lay out the required extension points in the form of abstract methods. This is handy as you can’t accidentally omit the required functionality when creating a new Value Object. Here’s the base class:
public abstract class ValueObject<T>
where T : ValueObject<T>
{
public override bool Equals(object obj)
{
var valueObject = obj as T;
if (ReferenceEquals(valueObject, null))
return false;
return EqualsCore(valueObject);
}
protected abstract bool EqualsCore(T other);
public override int GetHashCode()
{
return GetHashCodeCore();
}
protected abstract int GetHashCodeCore();
public static bool operator ==(ValueObject<T> a, ValueObject<T> 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<T> a, ValueObject<T> b)
{
return !(a == b);
}
}
And this is what the Value Object would look like after the extraction:
public class DecimalGreaterThanZero : ValueObject<DecimalGreaterThanZero>
{
private readonly decimal _value;
public DecimalGreaterThanZero(decimal value)
{
if (CanCreate(value) == false)
throw new ArgumentException("Value must be greater than zero.", nameof(value));
_value = value;
}
public static bool CanCreate(decimal d)
{
return d > decimal.Zero;
}
private decimal ValueGreaterThanZero => _value == decimal.Zero ? 1 : _value;
public static implicit operator DecimalGreaterThanZero(decimal d)
{
return new DecimalGreaterThanZero(d);
}
public static implicit operator decimal(DecimalGreaterThanZero decimalGreaterThanZero)
{
return decimalGreaterThanZero.ValueGreaterThanZero;
}
protected override bool EqualsCore(DecimalGreaterThanZero other)
{
return ValueGreaterThanZero == other.ValueGreaterThanZero;
}
protected override int GetHashCodeCore()
{
return ValueGreaterThanZero.GetHashCode();
}
}
Note that there are drawbacks to this approach as well. As we now inherit from the abstract base class, we have to change the Value Object from being a struct to class. This may potentially lead to performance issues, so if performance is a major concern in your application, you might want to skip this step. In all other cases, it’s generally a good practice.
I like how the author validates invariants in the class’s constructor:
public DecimalGreaterThanZero(decimal value)
{
if (CanCreate(value) == false)
throw new ArgumentException("Value must be greater than zero.", nameof(value));
_value = value;
}
public static bool CanCreate(decimal d)
{
return d > decimal.Zero;
}
This ensures that all instances of the Value Object are actually greater than zero.
There are two things that could be improved. Look at this property:
private decimal ValueGreaterThanZero => _value == decimal.Zero ? 1 : _value;
It wraps the private _value
field by checking if that value is 0 and if it is, returns 1 instead. This looks unnecessary. The _value
field cannot be zero because we check it in the constructor, and it also cannot be changed later on as the field is read-only. So, remove the field:
public class DecimalGreaterThanZero : ValueObject<DecimalGreaterThanZero>
{
private readonly decimal _value;
public DecimalGreaterThanZero(decimal value)
{
if (CanCreate(value) == false)
throw new ArgumentException("Value must be greater than zero.", nameof(value));
_value = value;
}
public static bool CanCreate(decimal d)
{
return d > decimal.Zero;
}
public static implicit operator DecimalGreaterThanZero(decimal d)
{
return new DecimalGreaterThanZero(d);
}
public static implicit operator decimal(DecimalGreaterThanZero decimalGreaterThanZero)
{
return decimalGreaterThanZero._value;
}
protected override bool EqualsCore(DecimalGreaterThanZero other)
{
return _value == other._value;
}
protected override int GetHashCodeCore()
{
return _value.GetHashCode();
}
}
The second issue is the implicit conversion from decimal to DecimalGreaterThanZero
:
public static implicit operator DecimalGreaterThanZero(decimal d)
{
return new DecimalGreaterThanZero(d);
}
Conversions that may lead to exceptions down the road should be made explicit. You don’t want this code to be possible:
DecimalGreaterThanZero value = 0;
Implicit conversions should be reserved only for operations that cannot possibly fail, like the backward conversion from the Value Object to a decimal:
decimal value2 = value1; // value1 is a DecimalGreaterThanZero
And we do have this backward conversion defined as an implicit operator, so we are good on this side.
As for the straight conversion, we need to change the modifier from implicit to explicit. This way, you show the client that there’s a potential information loss and that the client must be careful implementing this conversion as it might result in an exception:
DecimalGreaterThanZero value = (DecimalGreaterThanZero)0;
This is how the Value Object looks after all refactorings:
public class DecimalGreaterThanZero : ValueObject<DecimalGreaterThanZero>
{
private readonly decimal _value;
public DecimalGreaterThanZero(decimal value)
{
if (CanCreate(value) == false)
throw new ArgumentException("Value must be greater than zero.", nameof(value));
_value = value;
}
public static bool CanCreate(decimal d)
{
return d > decimal.Zero;
}
public static explicit operator DecimalGreaterThanZero(decimal d)
{
return new DecimalGreaterThanZero(d);
}
public static implicit operator decimal(DecimalGreaterThanZero decimalGreaterThanZero)
{
return decimalGreaterThanZero._value;
}
protected override bool EqualsCore(DecimalGreaterThanZero other)
{
return _value == other._value;
}
protected override int GetHashCodeCore()
{
return _value.GetHashCode();
}
}
Again, I personally would not introduce such a Value Object because the underlying business concept is too simple. But your mileage may vary, so mind the bigger context.
If you want to send your code for review, use the form on this page. By the way, you can send me unit tests too, especially those that heavily use mocks.
Summary
-
There are 4 characteristics when it comes to deciding on whether to introduce a Value Object for a domain concept:
-
The number and complexity of invariants
-
The number of primitives it embodies
-
Overall project complexity
-
The number of duplications
-
-
When there are more than 1 primitive needed to represent a domain concept, always extract them to a Value Object. Otherwise look at the other characteristics.
-
Use implicit operators only for conversions that cannot possibly fail. Use explicit operators in all other cases.
Subscribe
Comments
comments powered by Disqus