Practical .NET

Creating a True Value Object

You can dramatically simplify your code by using classes to define read-only/immutable objects … but to create classes that behave correctly requires a little bit of redirection.

In a previous column, I discussed how you could simplify your application by making more use of read-only/immutable value objects. I followed that up with a column on how to actually create those objects by using structs. But using structs has a problem: You have to give up one of the most powerful tools in an object-oriented developer's toolbox -- inheritance. This column shows how to create value objects using something with which you're more familiar: Classes. However, before doing that, I should provide some justification for using a Class rather than a struct.

Here's that justification: In the case study, I've assumed I have a SalesOrder object with a collection of Adjustment value objects. Adjustments represent changes to be made to the price of a sales order: They're used to move the price up or down depending on whether the client is entitled to a discount or has incurred some extra charge. Some discounts are percentage-based ("5 percent off for paying cash") while others are absolute amounts ("$5.00 off your second purchase"). The best way to deal with this difference is to create two kinds of Adjustment objects: a PercentageAdjustment and an AbsoluteAdjustment. If I create my value objects using classes, I can start with a base Adjustment class that defines what Adjustments look like (and hold any common code). I can then create PercentageAdjustment and AbsoluteAdjustment objects that inherit from my base Adjustment class.

Defining the Classes
To define my immutable/read-only base class I begin with a constructor that can't be used by the application program (that would make the resulting object mutable): I just declare the constructor as Friend (internal in C#). All I do in the constructor is set the properties in the Adjustment object:

Public Class Adjustment

  Friend Sub New(Name As String, Amount As Decimal)
    Me.Name = Name
    Me.Amount = Amount
  End Sub

Then I need to define the properties that every Adjustment object should have. These have private setters to make sure their values can't be changed by the application program (again, that would make the object mutable). Here's the Name property as an example:

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

To create my derived AbsoluteAdjustment class, I need to inherit from my Adjustment class. In the constructor for this derived class, I have to call the constructor for my base Adjustment class so that it can set its own properties. That code looks like this:

Public Class AbsoluteAdjustment
  Inherits Adjustment

  Friend Sub New(Name As String, Amount As Decimal)
    MyBase.New(Name, Amount)
  End Sub

In C#, the constructor looks like this:

public class AbsoluteAdjustment: Adjustment

  Internal AbsoluteAdjustment(string Name, decimal Amount): base(Name, Amount)
  {}

Of course, applications need a way to retrieve Adjustments. To support that, I also create an AdjustmentFactory class with methods that support returning Adjustments in the various ways required by applications. Listing 1 shows the code for that factory class with the methods declared as Shared (static in C#) so that developers don't have to instantiate the AdjustmentFactory to use its methods.

Listing 1: A Factory Class for Retrieving Immutable Adjustment Objects
Public Class AdjustmentFactory

  Public Shared Function GetAdjustmentByName(AdjustmentName As String) As Adjustment
    Select Case AdjustmentName
      Case "PayingCash"
        Return New AbsoluteAdjustment(AdjustmentName, 10)
      'other options
    End Select
  End Function

  Public Shared Function GetAdjustmentsForSalesOrder(OrderId As String) As List(of Adjustment)
    'code for retrieving all the adjustments for a SalesOrder
  End Function

End Class

This factory class would have to go in the same project as my Adjustment classes so that it has access to the Friend/internal constructors in my Adjustment classes.

To retrieve Adjustments, I write code like this in my application:

adjAbsolute1 = AdjustmentFactory.GetAdjustmentByName("PayingCash")

Redefining Equality
However, I now have a problem. It's important to me that two Adjustment objects with the same values in their properties be interchangeable. That means that two Adjustment objects with the same values in their properties should be equal to each other (see my first article on value objects for why I care about this). In other words, I want the test at the end of this code to be true:

Dim adjAbsolute2 As AbsoluteAdjustment
adjAbsolute1 = AdjustmentFactory.GetAdjustmentByName("PayingCash")
adjAbsolute2 = AdjustmentFactory.GetAdjustmentByName("PayingCash")

If adjAbsolute1 = adjAbsolute2 OrElse
  adjAbsolute1.Equals(adjAbsolute2) Then

Right now, this code won't even compile: You'll get a message that says that the equals sign (=) isn't even defined for Adjustment objects. Fortunately, I can solve this problem by redirecting the Microsoft .NET Framework to my own definition of what the equals sign means.

My first step is to add Operator methods to my AbsoluteAdjustment class to provide an implementation for the equals sign. That looks like the following code (for more about this, see my column on defining operators):

Public Shared Operator =(adj1 As AbsoluteAdjustment, adj2 As AbsoluteAdjustment) As Boolean
  Return adj1.Equals(adj2)
End Operator

Public Shared Operator <>(adj1 As AbsoluteAdjustment, adj2 As AbsoluteAdjustment) As Boolean
  Return Not adj1.Equals(adj2)
End Operator

Because these methods only accept AbsoluteAdjustment objects, I've ensured that I can't compare AbsoluteAdjustments to PercentageAdjustments. This is good because a $5.00 AbsoluteAdjustment object shouldn't be equal to a 5 percent PercentageAdjustment object.

Unfortunately, just calling the Equals method doesn't solve my problem. While my code will now compile, the test will return False even if all the properties in my objects have the same values (it's just the way classes work). So my final step is to override the Equals method that all classes inherit from the .NET Framework and put in code that returns True when all the properties have identical values:

Public Overrides Function Equals(obj As Object) As Boolean
  Dim adj2 As AbsoluteAdjustment
  adj2 = CType(obj, AbsoluteAdjustment)
  Return Me.Name = adj2.Name AndAlso
         Me.Amount = adj2.Amount
End Function

There's nothing technically wrong with using classes to implement value objects, but there is a long-term danger: What if later some developer adds a new property to my Adjustment class and forgets to add the corresponding test to the Operator methods? If that happens my Operator method could now, potentially, return True when two Adjustments have different values. Hopefully, I've put a test suite in place that will catch this. And, fortunately for me, because of the way I've written my solution, if I decide to change my implementation from using structs to classes, the application code that uses my value objects shouldn't notice the difference. Whatever differences there are between the two implementations are hidden inside my AdjustmentFactory class.

So which should you use when creating value objects: classes or structs? I think that, if you don't need inheritance, then structs are your best choice. And if you do need inheritance … well, it turns out that I do have a solution for structs that meets my goals of distinguishing between AbsoluteAdjustments and PercentageAdjustments. That's for a later column, though.

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
Most   Popular
Upcoming Events

.NET Insight

Sign up for our newsletter.

Terms and Privacy Policy consent

I agree to this site's Privacy Policy.