Value objects and How to Appreciate Them
A value object is a design pattern in which an object is made to represent something simple, like currencies or dates. A value object should be equal to another value object if both objects have the same value despite being two different objects. In this article, I will dive into why I find value objects useful and discuss various trade-offs in designing value objects. I will be doing examples in C#, but the examples should apply to many of the other major languages out there.
Shopping for value — a shopping cart example:
Imagine that you’re in charge of developing or maintaining the code base of a simple webshop. Every item in the store has a unique ID, name, and price associated with it. In this case, an item may look like this in code:
Suddenly a new requirement emerges: shop items may have their prices quoted in multiple currencies.
Considering the time to market, and the fact that you can feel your boss breathing down your neck, you may be tempted to introduce a dictionary. This way you can look up the price for each currency. Without much hesitation and under pressure to deliver, you opt to go for a simple dictionary from string to decimal:
Examining the change:
Let’s take a closer look at the change introduced above. From a quick look, it appears rather auspicious. It seems to be easy to wire up against a relational database. The change is easy to introduce, and the code indicates that — in order to get a price for an item — you must look up the price for it first. All good!
Not so fast. Fast forward a few months and a new developer onboards the project. When he sees the dictionary of prices, he may be puzzled by what exactly the key of the dictionary represents. Is it the regular price and the discounted ones from various coupon codes? Is it multiple currencies, or something else entirely? Besides, if the dictionary contains prices in different currencies then does he use “USD” or “$”?
The problem with the code is that it does not show the intent of usage in its definition. A workaround against this could be to encapsulate the dictionary and add a method named PriceForCurrency. While this change would indeed make the intent easier to read when it comes to lookup, all other operations on the dictionary are now also encapsulated and must be exposed if other places in the codebase require it.
Enter value object
This is where the introduction of a value object may be a viable option. To recap:
- The value object should represent something simple, such as the currencies in this example
- Two objects should be equal if their actual values are equal, not necessarily if they point to the same object.
- They should be immutable when possible.
A simple implementation of such a value object may look like:
When we now take a look at the price’s dictionary in the item object, it is clear what the intent is. It is mapping from currencies to their associated prices. The price we paid is introducing an extra type of object that does little but hold a string value. The object introduced adheres to the value object principles. The object represents something simple, equality between objects is determined by their values, and — finally — objects are immutable. On top of that, it overrides the ToString method so it displays nicely as a string in the debugger.
So all is good right? The answer is that it depends. If the first programmer had written the currency object before anyone else would have a chance to use it in the codebase, then this solution may suffice. But if a new programmer introduces this change, it may start breaking all sorts of things in the codebase. The programmer will either have to navigate to all the use sites and change the code so it compiles, roll back the change, or come up with something clever. If you work in an organization that has a code review policy, this can quickly become one of those very big tiresome pull requests — only to introduce an extra layer of abstraction.
Examining the currency object:
So the new programmer has just introduced the currency type and thousands of compile errors start appearing. While initially discouraging, this is actually a good thing. It means that the compiler and its type system are doing their job. Nevertheless, all these errors will still require attention, effort, and time to fix. But the new developer remembers that most of these errors only occur because people expect the items to be strings and so were allowed to perform string operations on them. Chances are a good chunk of errors will look something like this:
Operator '==' cannot be applied to operands of type 'Currency' and 'string'Argument 1: cannot convert from 'string' to 'ValueObjects.Currency'
When inspecting the call sites, they may look at bit like this:
The developer can mitigate these issues in several different ways. The first kind of error can be fixed by adding an implicit cast from currency to string. However, to solve the second issue, an implicit cast from string to currency may be in order. Arguments can definitely be made against implicit casts, as excessive use of these may hide what is actually going on in the code. But it may offer a gentler introduction of the currency type into the codebase. The developer can also mark the implicit cast function as obsolete so that the compiler errors turn into compiler warnings. Most IDE’s will subsequently highlight these warnings, thereby inviting other developers to change their code without forcing them to.
Introducing value objects in an existing code base is a bit of an art. Whether or not to introduce the implicit casts to ease the migration or to add the obsolete attribute to trigger compiler warnings all comes down to sound judgment, the condition of the existing code base, coding standards, and continuous integration tools.
Design and performance considerations
One may conclude that it seems like an awful lot of work just to hide a string inside another type but by introducing your own type you now have increased control over validation and data layout. You can perform extensive validation in the constructor to prevent the construction of currency objects with invalid data. Also, you can change the underlying representation of the currency object if you find a more suitable one.
A word of warning, before reading on. Before doing performance optimization, always check that you actually have a performance problem before doing it, then make sure to measure what takes the time and where the problem lies.
One initial concern one may have with the above approach is that not only is it an extra layer of abstraction, it is also an extra layer of indirection. This is because classes are reference types and hence heap-allocated, whereas structs instead are value types, can be stack-allocated, and inlined into arrays. This may reduce pressure on the garbage collector.
The advantage of classes is that you can control initialization completely. Either the object is constructed through the constructor of your choice, or it ends up being null. Structs don’t have that luxury. The good thing about structs is that they disappear when the stack frame is popped and does not need garbage collection. Choosing which route to choose depends on your use case and whether or not your value object will have a sensible state when initiated with a default constructor. In the case of the currency object, an argument could very well be made for rolling with a struct, as instantiating a currency with null leaves you no worse off than the previous state where currencies were represented as strings.
Now consider the case where you have lots of items with prices available in lots of different currencies. In the current implementation this will lead to many strings of “USD” and “EUR” etc. being allocated. In this case, you may opt to have the currency value object do string interning in the currency constructor. String interning may be useful when you have lots of strings with the same value. In the code example below, every instance of a currency object that is constructed with “EUR” will point to the same “EUR” string.
This is the advantage of abstraction after all. You get the power to hide implementation details and optimizations from the users of your datatypes.
Conclusion:
Value objects can be introduced into your codebase in many different shapes and varieties. Whether or not the trade-off is worth it is up to you and your coworkers to decide. I hope to have shown you how value objects can add readability along with stronger, more flexible data types. Finally, I hope that they can be introduced gently into your codebase with minimal extra overhead.