Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Languages / XML

Modelling Attributes Using Value Objects

4.75/5 (18 votes)
4 Apr 2012CPOL15 min read 31.3K   126  
Look beyond fundamental data types when modelling object attributes

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 Longs or Strings. 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 strings 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:

VB
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.

VB
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.

VB
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:

VB
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.

VB
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:

VB
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.

VB
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.

VB
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.

VB
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.

VB
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.

VB
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.

VB
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 scales, 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:

VB
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 Objects that you may already be familiar with.

The fundamental data types in .NET are all implemented as objects. Strings 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.

VB
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:

VB
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.

VB
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 Objects 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.

VB
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
    ' Calculate New Value Here
    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.

VB
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.

VB
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 strings have operators defined for them. Not all operators make sense for all data types. You can concatenate two strings using + or &, but it makes no sense to define a multiply (*) or divide (/) operator for strings.

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 Temperatures. The function should also return a temperature (the result of adding or subtracting the two temperatures that were provided).

VB
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.

VB
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 Temperatures, we convert them both to the same scale, then compare the values.

VB
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(=).

VB
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 (<):

VB
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.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)