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. A good example are attributes. 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 it’s own right.
Attributes might also jump off the page as Nouns, but because they're "only attributes" they aren't taken as seriously. 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 DataTypes Fundamental datatypes 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 datatypes and end with an fully 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 datatype. 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 datatypes and created our own, we’re already seeing benefits. The relationship between the temperature value and 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 it’s value and scale in the first place is a seperate 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 more serious limitation of our current solution is that there is nowhere to put logic. A structure is purely a data structure. If we want to implement conversion functions from one scale to another, we would like to keep the logic with the data. Using a class instead of a structure will allow us to solve both these problems.
Attributes as Objects A simple conversion from Structure to Class is easy. Public Class 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 Class
To fix the problem of having to set the Value and Scale seperately we've added a constructor that accepts both the value and scale. The constructor simply sets the two class member variables with the values of the parameters passed. 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 value or scale and leaving the other untouched. It might seem odd to take away flexibility from a user of our class, 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.
Right now we have a way for a consumer of our class to set the value and scale when instantiating the class. 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 Class 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 Class
Now let’s look at adding the logic for converting from one scale to another. For each of the 4 scales, there are 3 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 tempvariable. 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 We can take our Temperature object further by turning it into a Value Object. 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 is, and at some Value Objects that you may already be familiar with.
The fundamental datatypes 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. 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 it’s values in its constructor, and which does not allow those values to be modified again after that point. Any methods 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 lets 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 guarentee 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 ' Calculate New Value Here Return New Temperature(newTemp, scale) End Function
With this done, the ways that 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 it’s 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 seperate 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 datatypes like integers, doubles and even strings have operators defined for them. Not all operators make sense for all datatypes. 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
Miltiplication 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 returns 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 operators 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 class that accepts two Temperatures. The function should also return a temperature (the result of adding or subtracting the two Temperatures 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 Temperatures 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 it’s 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
And that’s that for Value Objects. A sample project can be downloaded from here. |