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

  • Uno Platform Ports Windows Calculator to Linux

    Uno Platform has ported the famed Windows Calculator, open sourced last year, to Linux as part of a continuing "proof point" effort to demonstrate the reach of what it describes as the sole UI offering available to target Windows, WebAssembly, iOS, macOS, Android and Linux with single-codebase applications coded in C# and XAML.

  • ASP.NET Core OData 8 Preview Supports .NET 5, but with Breaking Changes

    ASP.NET Core OData, which debuted in July 2018, is out in a v8.0 preview that for the first time supports the upcoming .NET 5 milestone release.

  • VS Code Java Team Details 5 Best Dev Practices

    Microsoft's Visual Studio Code team for Java development added a new Coding Pack for Java installer and detailed best practices for setting up a development environment.

  • Binary Classification Using PyTorch: Defining a Network

    Dr. James McCaffrey of Microsoft Research tackles how to define a network in the second of a series of four articles that present a complete end-to-end production-quality example of binary classification using a PyTorch neural network, including a full Python code sample and data files.

  • Blazor Debugging Boosted in .NET 5 RC 2

    In highlighting updates to ASP.NET Core in the just-launched second and final Release Candidate of .NET 5, Microsoft pointed out better debugging for Blazor, the red-hot project that allows for C# coding of web projects.

Upcoming Events