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:
you will be able to write just this single line:
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:
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:
And this is what the Value Object would look like after the extraction:
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:
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:
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:
The second issue is the implicit conversion from decimal to DecimalGreaterThanZero:
Conversions that may lead to exceptions down the road should be made explicit. You don’t want this code to be possible:
Implicit conversions should be reserved only for operations that cannot possibly fail, like the backward conversion from the Value Object to a decimal:
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:
This is how the Value Object looks after all refactorings:
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.
- 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.