Introduction
When it comes to modeling objects, simple attributes often fail to get the consideration they deserve. The lazy option is to model attributes using fundamental data types like Long
s or String
s. This misses the opportunity to create far more meaningful and robust code. This article takes the example of a Temperature
attribute and works from a naive implementation towards a more object oriented approach. Along the way, we'll learn the benefits of Value Objects and how to implement them.
Background
One of the first Object Oriented techniques that most of us learn is to "Find the Nouns". Entities like Customer
, Invoice
, and Product
jump off the page of the requirements spec, and off we go.
The problem with this approach is that it conditions us to focus on one type of object while ignoring other very useful types. Attributes are a good example of this. An attribute is a property or characteristic of another object. They differ from entities in that they don't have their own identity. So, while a customer is an entity and has an identity, the credit score of the customer is an attribute. It's just a number, it has no identity in its own right.
Attributes might also jump off the page as Nouns, but because they're "only attributes", they can be glossed over. In our haste to identify entities and implement them as objects, we often represent attributes using the first data type that comes to mind. Credit score? An integer should do. Address? A couple of string
s will do the trick. Account balance? A Double
if we need cents, a Long
if we don't.
Beyond Fundamental Data Types
Fundamental data types also suffer from the problem that they store only one value. In real solutions, attributes are often a combination of a number of values. A temperature contains a value and a scale (100 degrees Fahrenheit). An address might contain multiple lines of text, a zip code, a state, etc. so even these very simple examples can't be solved with a single fundamental data type.
Over the remainder of this article, I'm going to implement a Temperature
attribute which has both a Value
and a Scale
. We'll start out with a naive implementation using fundamental data types, and end with a full fledged object that not only represents a temperature but opens up a world of possibilities.
Let's start with the naive implementation. We could use two distinct data types, a double
for the value
and a string
for the scale
. This works, but it means always having to remember to keep the two values together. For example, if we need to pass the temperature to a function, we need two parameters. If a class has a temperature, we would need to implement it as two properties:
Public Class WeatherReading
Public TemperatureValue As Double
Public TemperatureScale As String
End Class
Assigning a value to the temperature attribute becomes a two step process.
Dim reading As New WeatherReading
reading.TemperatureValue = 100
reading.TemperatureScale = "Fahrenheit"
The only thing about these properties that tells us that they are related is the use of the word 'Temperature
' in the name. We can do better than this. If we use a structure, we can explicitly group the value and scale into a single Temperature
data type.
Public Structure Temperature
Dim Value As Double
Dim Scale As String
End Structure
Public Class WeatherReading
Public Temperature As Temperature
End Class
So, having stepped away from the fundamental data types and created our own, we're already seeing benefits. The relationship between the temperature value and the temperature scale is now explicit. We can handle our temperature data as a single variable:
Public Sub foo(ByVal temp as Temperature)
End Sub
Assigning a value to the Temperature
property of a class is now a one step process.
Dim reading As New WeatherReading
reading.Temperature = temp
How the temp
variable was assigned its value
and scale
in the first place is a separate issue, we'll look at that later. For now, we're just happy that a temperature can be worked with as a single variable.
There are still problems, of course. I don't like the fact that scale
is a string
, it allows someone using our class to enter anything:
reading.Temperature.Scale = "Miles"
An enum
is an ideal way of representing a discrete set of values. In this case, our enum
has an entry for each of the various temperature scales that we need. If we expand our system later to handle more scales, we can add them to the enum
.
Public Enum TemperatureScale
Celsius = 1
Fahrenheit = 2
Kelvin = 3
Rankine = 4
End Enum
Having defined the enum
, we can modify our structure to use it.
Public Structure Temperature
Dim Value As Double
Dim Scale As TemperatureScale
End Structure
With this done, we can only assign members of the enum
to the scale.
reading.Temperature.Scale = TemperatureScale.Fahrenheit
We also get the benefit of intellisense. Visual Studio will automatically list the various scales when we need to choose one. Many little improvements like this compounded together make our code more usable, more readable, and more maintainable.
Let's back up a bit. I mentioned earlier that while we can work with a temperature as a single variable, that variable still has to be assigned both a value and scale. So, we still have the problem that creating a temperature variable is a two step process, one assignment for the value
and another for the scale
.
Another niggling concern with our current solution is that Temperature
is purely a data structure. We are neglecting the possibility of bundling some useful logic with that data. The ability to compare two temperatures, or convert between different temperature scales would be very useful.
In a pre-object oriented world, we might have used a structure to represent our data and then used separate functions to operate on the data. If we think in terms of objects, we can build some of that behaviour into our Temperature
object.
When I originally wrote this article, I incorrectly suggested that when you start adding logic to structures, you should switch to using a class rather than a structure. That was incorrect.
A structure can provide most of the features of a class such as constructors, methods, etc. Structures can even implement interfaces, but structures don't do implementation inheritance. That's not as big a loss as you might think. Inheritance is often overused and abused, and for the kinds of objects that suit structures, Inheritance is generally not a good idea.
There isn't a clear cut line delineating where structures should be used and where classes are a better option. You'll find various rules of thumb, but frankly, it's not something you should get too hung up on.
Our Temperature
is small, it has only two member fields, its data is simple, no references to other objects, just a numeric value and an Enum
. On the face of it, it's a perfect candidate for a structure.
For the purposes of what we're doing here, a structure and a class will work identically. Change the word 'Structure
' to 'Class
' in the code and you're done. If you decide to create and manipulate a few thousand temperatures, you may find that the structure gives better performance. This is mainly down to differences in the way structures and classes are instantiated.
For now, we'll stick with a structure.
Attributes as Objects
Let's start by adding a constructor to our structure.
Public Structure Temperature
Public Value As Double
Public Scale As TemperatureScale
Public Sub New(ByVal newValue As Double, ByVal newScale As TemperatureScale)
Value = newValue
Scale = newScale
End Sub
End Structure
The constructor accepts both the value
and scale
and sets the two class member variables with the values of the parameters passed. Now we can instantiate our Temperature
and set both the Value
and Scale
all at once.
Dim temp As New Temperature(96.5, TemperatureScale.Fahrenheit)
Value
and Scale
are still public
members of the class, which means that programmers using our class can still modify them directly. I'd like to stop them from modifying just the value
or scale
and leaving the other untouched. It might seem odd to take away flexibility from the user of our object, but that is precisely what we should do.
Flexibility should be something that we build into our code deliberately. It should not happen by default simply because we didn't give it any thought. More flexibility means more ways of using the object, and more ways of combining objects. All this leads to more potential test cases, and more subtle bugs. It's better to start by exposing as little functionality as possible and add more as required.
If you really push this concept, you will be amazed just how much behaviour you can keep within your classes, hidden from the outside world. Give it a try, when you think you've hidden all you can hide, look again.
Right now, we have a way for the consumer of our structure to set the value
and scale
when instantiating a Temperature
. We know that we want to build in a mechanism for converting from one temperature scale to another. We have no need right now for direct access to the value
and scale
. So, let's remove that option; if the need arises, we can always add it again.
Public Structure Temperature
Private _Value As Double
Private _Scale As TemperatureScale
Public ReadOnly Property Value() As Double
Get
Return _Value
End Get
End Property
Public ReadOnly Property Scale() As TemperatureScale
Get
Return _Scale
End Get
End Property
Public Sub New(ByVal value As Double, ByVal scale As TemperatureScale)
_Value = value
_Scale = scale
End Sub
End Structure
Now, let's look at adding the logic for converting from one scale
to another. For each of the four scale
s, there are three others that we may wish to convert to. That's 12 conversion routines. Each new scale
we add will cause the number of conversion routines to mushroom.
Let's leave the specifics of the conversion routines aside and think about how a programmer might use our finished class. One approach might be something like the following:
Dim temp As New Temperature(96.5, TemperatureScale.Fahrenheit)
temp.ConvertTo(TemperatureScale.Celsius)
That's pretty simple. A single function handles the conversion. Behind the scenes, there may be numerous functions to convert to and from each scale, but someone using our class shouldn't have to worry about that.
Note that the ConvertTo
routine will modify the value and scale of the temp
variable. Earlier, we made value
and scale
read only to prevent consumers of our class from modifying them. The ConvertTo
routine does modify them, but in a controlled way. This is an example of exposing just enough functionality and no more.
A Value Object
Our structure as it stands is effectively a Value Object
. It represents a value
rather than an entity
with its own identity, and it adds some useful behaviour. In reality, however, we need to further before we have implemented a true Value Object. We need to make it immutable. This means that once we create a temperature, we won't be able to change it in any way.
How can this work? What about our 'ConvertTo
' function? What is the point of a change like this? Let's start to answer these questions by looking at what a Value Object
really is, and at some immutable Value Object
s that you may already be familiar with.
The fundamental data types in .NET are all implemented as objects. String
s and integers have methods. If you play with these objects and methods, you'll notice an interesting thing. There is no method that you can call that changes the value of the variable. Let's take a look at a classic example. The following code looks like it would modify the str
variable (replacing B with D), but it doesn't.
Dim str As String = "ABC"
str.Replace("B", "D")
If we want to modify the str
variable to replace B
with D
, we would need to do the following:
Dim str As String = "ABC"
str=str.Replace("B", "D")
The difference is subtle but very important. The Replace
method doesn't modify the string
, it creates a completely new string
. Other methods like ToLower
and ToUpper
work in exactly the same way. This makes sense. If you change a string
, it is no longer the same string
; by definition, you are dealing with a completely new string
.
So, a Value Object
is one that is assigned its values in its constructor, and which does not allow those values to be modified again after that point. Any method that transforms the object should return a completely new instance of the object.
In our Temperature
example, we can still use our ConvertTo
function, but instead of having it modify the value
and scale
of the temperature, it should return a completely new Temperature
object with the appropriate value
and scale
.
Dim temp As New Temperature(96.5, TemperatureScale.Fahrenheit)
temp = temp.ConvertTo(TemperatureScale.Fahrenheit)
That's the how; now, let's look at the why. The first thing we need to acknowledge is that Value Object
s capture an aspect of the real world. Values like temperatures don't have an identity. If a temperature increases from 20 degrees to 30 degrees, it is no longer the same temperature. We don't think of the number 6 as a modified version of the number 2. They are different numbers.
An entity like a Customer
or Employee
can change in various ways and still be the same Customer
or Employee
. When we use a Value Object
, we send a message to anyone reading our code. We draw attention to the difference between a value and an entity.
Value
objects are also another example of making things easier by reducing options. If we know for a fact that an object can't change, it gives us confidence, it narrows down the range of things that can go wrong, and makes debugging easier.
To see this in action, consider what happens when we pass an object as a parameter to a routine. If we pass a parameter ByRef
, we accept that it can be changed; a parameter passed ByVal
can not be changed. When we pass objects, these rules don't really hold.
When we pass an object, what we actually pass is the address of the object. This means that ByRef
and ByVal
don't work as we might expect. An object address passed ByRef
can be changed, which means that when the routine ends, the variable which was passed as a parameter could be pointing to a completely different instance of the object.
An object address passed ByVal
can not be changed, which means that when the routine ends, the parameter will still be pointing to the same instance of the object. The routine could still do something that changes the state of the object, so passing ByVal
is no guarantee that our object will remain unchanged.
If we implement an object as a Value Object
, then we know that it can't be modified. ByVal
and ByRef
behave as expected. A Value Object
passed ByVal
will not change regardless of what the routine does. A Value Object
passed ByRef
can be made to point to a new object
instance.
Implementing a Value Object
couldn't be simpler. We provide a constructor that sets the member variables of the class, and we provide no other means of modifying them. Our conversion function should return a brand new Temperature
object. And that's all there is to it.
Public Sub New(ByVal value As Double, ByVal scale As TemperatureScale)
_value = value
_scale = scale
End Sub
Public Function ConvertTo(ByVal scale As TemperatureScale) As Temperature
Dim newValue As Double
Return New Temperature(newTemp, scale)
End Function
With this done, the ways in which we can interact with our temperature object become a little more limited, but the things we can achieve with it aren't limited at all. We can still create a temperature, and convert it to any scale.
Dim a As New Temperature(25, TemperatureScale.Celsius)
Dim b As New Temperature = a.ConvertTo(TemperatureScale.Fahrenheit)
After the code above has run, the variable a
still has the same value and scale (25 Celsius). Nothing you can do to a
will change its value
. What you can do is point a
to a brand new Temperature
object, and that object can be a product of a
itself.
Dim a As New Temperature(25, TemperatureScale.Celsius)
a = a.ConvertTo(TemperatureScale.Fahrenheit)
The only difference between this example and the one above it is that instead of using a separate variable to hold the result of the conversion, we store the result back in the same variable. This shouldn't be too big a mental leap, we do this all the time with object values. Consider the following two lines of code.
b = a + 1
a = a + 1
Overloading Operators
There is an interesting difference between this last snippet of code and the previous code, and that is the use of the '+
' operator. The previous example uses a function 'ConvertTo
'. In reality, the two ways of doing things are basically the same. The '+
' operator is really a function that takes in two parameters, adds them together, and returns the result as a new value.
a+b : add(a,b)
a-b : subtract(a,b)
Fundamental data types like integers, doubles, and even string
s have operators defined for them. Not all operators make sense for all data types. You can concatenate two string
s using +
or &
, but it makes no sense to define a multiply (*
) or divide (/
) operator for string
s.
This operator notation looks like it might be useful for our Temperature
object. Wouldn't it be great to be able to add two temperatures together without caring about whether they were the same scale? How about checking if one temperature was greater than another?
Of the various operators available in .NET, the following look like they might be useful for our Temperature
.
Arithmetic operators are used to perform arithmetic operations that involve calculation of numeric values.
+ : Addition
- : Subtraction
Multiplication and division could be done, but I can't think of any reason why I'd want to multiply or divide one temperature by another. Comparison operators compare operands and return a logical value based on whether the comparison is true or not.
= : Equality
<> : Inequality
< : Less than
> : Greater than
>= : Greater than or equal to
<= : Less than or equal to
Implementing a mathematical operator involves creating a public
function on our value
object which accepts two parameters of the same type as the value
object itself. In English, we need a function on our Temperature
structure that accepts two Temperature
s. The function should also return a temperature
(the result of adding or subtracting the two temperature
s that were provided).
Public Overloads Shared Operator +(ByVal a As Temperature,
ByVal b As Temperature) As Temperature
Dim interimB As Temperature = b.ConvertTo(a.Scale)
Return New Temperature(a.Value + interimB.Value, a.Scale)
End Operator
We need to make some decisions about how we implement the mathematical operators. It's not as straightforward as you might think. If we add a Celsius
and a Fahrenheit
temperature, what should the scale of our result be?
As a convention, we'll assume that the resulting scale will be the same as the leftmost operand. So if 'a
' is Celsius and 'b
' is Fahrenheit, then a + b
will result in a Celsius value. With this convention sorted, our method of adding is simple. We convert b
to the same scale as a
, then add their values together.
Dim interimB As Temperature = b.ConvertTo(a.Scale)
Return New Temperature(a.Value + interimB.Value, a.Scale)
Subtraction works in the same way. Again, the result will take the same scale as the leftmost value.
A comparison is implemented in a very similar way. We write a function that accepts two Temperature
parameters. The only difference is that the result of a comparison operator is a Boolean
rather than another Temperature
. To compare two Temperature
s, we convert them both to the same scale, then compare the values.
Public Overloads Shared Operator =(ByVal a As Temperature, _
ByVal b As Temperature) As Boolean
Dim interimB As Temperature = b.ConvertTo(a.Scale)
Return a.Value = interimB.Value
End Operator
Once we've written an operator, we can use it to implement its opposite. Not Equal (<>
) is the opposite of Equal(=
).
Public Overloads Shared Operator <>(ByVal a As Temperature, _
ByVal b As Temperature) As Boolean
Return Not a = b
End Operator
Greater than or Equal (>=
) is the opposite of Less than (<
):
Return Not a < b
Operator overloading is another feature that works exactly the same way for both Structures and Classes.
And that's that for Value
Objects.