Practical .NET

Simplifying Your Object Model with Value Objects

Our applications are complicated enough without adding any more. Here's a way to think about your objects that allows you to dramatically simplify your object model, provided you're willing to turn your current practice around by 180 degrees.

Some objects in your object model need a certain level of sophistication: Typically, you must track them though the application, passing them from one method to another. They also typically have properties you can change and, as a result, have data that must be validated and saved back to the database. If shared in an asynchronous, multi-threaded application they open your application to race and deadlock conditions. Let's call these "entity" objects because they probably represent real entities in your organization.

Other objects are much simpler. You can create or destroy them as you need and you never change their values (at least, in your application). If they're used on multiple threads, you can simply give each thread its own copy of the object. Let's call these "immutable" or "value" objects because all you're interested in is their data/values.

Immutable/value objects are simpler to create than entity objects. Typically, all you need is a set of read-only properties and a constructor that accepts all of the values needed to populate those properties. Any methods included in a value object are just there to provide those values in useful ways, but don't change or update the object (a value object might have a method that returns a time value for different time zones, for example). Because immutable objects don't change you don't have to worry about ensuring their values are consistent across threads.

Despite their simplicity, immutable objects are an unusual beast to find in most applications -- which is interesting because, as a .NET Framework developer, you work with immutable objects all the time. While the most famous immutable class is the String, the Integer class is also immutable. When you change the value in an Integer variable you're not really changing the value, you're replacing it. Unlike most objects, you don't hang on to some Integer object that currently has a value of four but later on has a value of 101 -- when you change the value of an Integer you get a whole new Integer object.

Guidelines for Value and Entity Objects
Our lives would be much simpler if we built our application with more value objects; provided, of course, we can figure out when creating a value object is the right thing to do. In other columns I've discussed a SalesOrder object designed to support the pricing process in the Billing domain. That object looks like this:

Public Class SalesOrder
  Public Property Id As String
  Public Property CustomerOwner As Customer
  Public Property Details As List(of SalesOrderDetails)
  Public Property Adjustments As List(of Adjustment)
End Class

In this design, I would classify the Customer object as an entity object, but classify the Adjustment objects, which contain information about how the price is to be discounted or marked up, as much simpler value objects. To make that decision, I followed a number of guidelines.

My first guidelines is that if the values in the object's properties define the object, then it's probably a value object. To put it another way: If I change the value of a property and the object becomes a different item, then it's probably a value object. For example, changing the discount/markup amount on an Adjustment gives me a different Adjustment; changing the address on a Customer object means that my customer has moved, it does not mean that I have a different customer.

The corollary of that distinction is that entity objects have an identity; value objects do not. The Customer object is the same customer even as the object's values change. That's not true with Adjustment objects: If I passed an Adjustment object to a method for processing and the method handed me back a completely different Adjustment object but with the same values in all its properties, I wouldn't care. With an Adjustment object, all I'm interested in are the amount of the discount or markup. However, if I passed a Customer object to a method and that method handed me back a completely different Customer, I'd be very upset: In this domain, some Customers are eligible for some discounts and others are not.

That example implies another guideline: Does the object connect me to other information? Customers connect me to a customer's contract, which supplies information about what discounts a customer is eligible for. An Adjustment connects me to … nothing else.

Returning to the issue of identity gives me another test: Can objects with identical values be interchanged freely? If I have two Customer objects with exactly the same information, I might be suspicious that there was a data entry error of some kind. Even then, it's still entirely possible that these objects represent two different (though very similar) customers. On the other hand, if I have two Adjustments with exactly the same information, I could discard one (or substitute one for the other). Another way of looking at this guideline is whether a value comparison makes sense: If all the values of all of the properties of the two objects are identical, do I consider them equal?

The identity issue also suggests that if I track the object's information across time and space (as Billing does with Customers to ensure that the bills are, eventually, paid) then it's an entity object. However, if I just use whatever data is available in the object and then discard that object when processing is complete (as is the case with Adjustment objects in the pricing process), then I'm more likely to be using a value object.

Think of value objects like you think of a dollar bill: Within the domain of "paying for lunch," do you really care which bill you use? Of course, the US Mint does see each dollar bill as a separate item (that's why they put serial numbers on them) -- but that's an entirely different domain than "paying for lunch".

Changing Your Habits
Unfortunately, we create entity objects even when we don't need the extra complexity because it's tempting to think, "Well, right now, adjustment is a value object. But some day in the future I might need to track adjustments -- I'd better make Adjustments an entity object." And that's certainly possible. It's not hard to imagine a different domain (one that keeps track of how adjustments are used and how their frequency of use affects revenue) that would need Adjustments to be an entity object. But that's another domain and I don't need to design for it in the Billing domain. With DDD, you don't try to support anything more than the demands of the domain in which you're working.

This distinction turns common practice on its head: Value objects, which are easier to write and work with, should be your first choice when designing objects. You should create entity objects only when you have to. The more complicated your objects are, the sooner your application will become impossible to maintain and have to be replaced rather than enhanced (the CRAP cycle of Create, Repair, Abandon, Replace)

You probably need a lot fewer entity objects than you think. After all, why not create the simplest possible objects? You have enough problems already.

About the Author

Peter Vogel is a system architect and principal in PH&V Information Services. PH&V provides full-stack consulting from UX design through object modeling to database design. Peter tweets about his VSM columns with the hashtag #vogelarticles. His blog posts on user experience design can be found at http://blog.learningtree.com/tag/ui/.

comments powered by Disqus

Featured

  • Compare New GitHub Copilot Free Plan for Visual Studio/VS Code to Paid Plans

    The free plan restricts the number of completions, chat requests and access to AI models, being suitable for occasional users and small projects.

  • Diving Deep into .NET MAUI

    Ever since someone figured out that fiddling bits results in source code, developers have sought one codebase for all types of apps on all platforms, with Microsoft's latest attempt to further that effort being .NET MAUI.

  • Copilot AI Boosts Abound in New VS Code v1.96

    Microsoft improved on its new "Copilot Edit" functionality in the latest release of Visual Studio Code, v1.96, its open-source based code editor that has become the most popular in the world according to many surveys.

  • AdaBoost Regression Using C#

    Dr. James McCaffrey from Microsoft Research presents a complete end-to-end demonstration of the AdaBoost.R2 algorithm for regression problems (where the goal is to predict a single numeric value). The implementation follows the original source research paper closely, so you can use it as a guide for customization for specific scenarios.

  • Versioning and Documenting ASP.NET Core Services

    Building an API with ASP.NET Core is only half the job. If your API is going to live more than one release cycle, you're going to need to version it. If you have other people building clients for it, you're going to need to document it.

Subscribe on YouTube