Practical .NET

Creating a Genuine Value Object

Peter addresses reader's concerns by showing how to implement a read-only/immutable value object.

In a previous column I discussed all the reasons for creating read-only objects and how they could simplify your applications (in fact, I recommended that creating a read-only object should be your first choice). I've been getting some (justified) flak from readers because in that column (and elsewhere) I keep referring to "read-only objects." With that phrase, I've actually been conflating two related-but-different ideas. In this column, I'll finally break down and tell you the truth. I'll discuss two different strategies for creating these objects, why I prefer one over the other, and show some best practices in creating and using them.

For this case study I'm looking at an application that calculates the price of a SalesOrder. Obviously, the SalesOrder object (or the invoice derived from it) needs to be updateable … but the rest of the inputs to the process don't need to be except, of course, when they're first created. For example, in this case study a SalesOrder has a group of Adjustment objects associated with it where each Adjustment represents how the price should be incremented or decremented (sometimes this is a percentage, sometimes an absolute dollar amount). These Adjustment objects can be immutable/read-only.

Two Different Implementations
What my readers have been pointing out is there are two ways to implement what I've been referring to as a "value object": You can use a class or a struct (if you're interested in the difference between a struct and a class, see "Structs vs. Classes: Why Should I Care?").

For these two implementations there are two different names. If you create your read-only object using a struct then you're creating a read-only "value type"; if you do it with a class then you're creating a read-only "immutable object."

When creating what I've called a value object, I prefer value types because of the equality test: Two structs with the same values in all of their properties are considered equal (at least, if you write the code correctly). Value objects are like the one dollar bills you use to pay for lunch: You really don't care which dollar bill you use because, as far as you're concerned, all one dollar bills are identical. In domain-driven development speech, you'd say that in your domain the one dollar bill is interchangeable with any other one dollar bill because they have the same value: $1. In other domains, of course, this isn't true: The U.S. Mint cares very much which one dollar bill is which and even assigns a unique identifier to each bill.

To put it in programmer speech, you want the test at the end of this code to be True:

Dim dol1 As Money
Dim dol2 As Money

dol1 = GetMoney(1.00)
dol2 = GetMoney(1.00)

If dol1.Equals(dol2) Then

That sums up value types for me.

Creating a Value Type
If I do use a struct, then my Adjustment object looks like the code in Listing 1.

Listing 1: Adjustment Object Code Using a Struct
Public Structure Adjustment

  Friend Sub New(Name As String, Amount As Decimal, 'more parameters)
    Me.Name = Name
    Me.Amount = Amount
    'setting more properties
  End Sub

  Private _Name As String
  Public Property Name As String
    Private Set(value As String)
      _Name = value
    End Set
    Get
      Return _Name
    End Get
  End Property
  
  Private _Amount As Decimal
  Public Property Amount As Decimal
    Private Set(value As Decimal)
      _Amount = value
    End Set
    Get
      Return _Amount
    End Get
  End Property
  'more properties

End Structure

Because the setters on my properties are declared as private, the only way to set the values of the properties on this struct is through the struct's constructor (or other code inside the struct). I've also, however, declared the constructor as Friend to ensure that the constructor can only be used through code that's in the same project/class library as the Adjustment (in C#, I'd declare the constructor as internal). Effectively, therefore, there's no way for a client to create or alter an Adjustment.

But, of course, clients do need to be able to get Adjustments. To support that I create an AdjustmentFactory class with methods that accept various parameters and return the appropriate Adjustment(s). This factory class will need to be in the same project/class library as my Adjustment class so that it can use the Adjustment class' constructor. The start of that factory class might look like the code in Listing 2 (the equivalent code in C# would use static rather than Shared).

Listing 2: A Factory for Value Types in Visual Basic
Public Class AdjustmentFactory
  Public Shared Function GetAdjustmentByName(AdjustmentName As String) As Adjustment
    Select Case AdjustmentName
      Case "PayingCash"
        Return New Adjustment(AdjustmentName, 10)
      'other options
    End Select
  End Function

  Public Shared Function GetAdjustmentsForSalesOrder(SalesOrderId As Integer) 
    As List(Of Adjustment)
  // Code to retrieve and return all the adjustments for a salesorder
  End Function

A client would use code like this to get an Adjustment:

Dim adjust1 As Adjustment
adjust1 = AdjustmentFactory.GetAdjustmentByName("PayingCash")

And now I have a true, immutable value type. But, to recognize that others may prefer to create classes with read-only properties, I'll probably continue to refer to "value objects." There's also more you should probably do with your value object -- it would be nice to use the equal sign (=) with tests, for example. I'll return to this topic in a later column.

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