Building Better Properties - Part II
A deeper look into building properties in .NET
In my Article titled "Building Better Properties" I made a case for 'why' using properties is the 'right' way to build objects as opposed to using public variables. If you come from VB6, it's very easy to mistakenly believe that public variables are a time saving shortcut to providing access to members of your class. When I first came to .NET, I preferred properties due to my background in C++ but if someone would have truly grilled me on why properties were better, I wouldn't have been able to make a compelling argument other than "That's what my professors told me to do in college so I wouldn't break encapsulation". Today I'm embarrassed I was so unopinionated about the subject.
Calculated Fields
Let's say that I had a class that represented a BillingItem. I'd have another object called Bill that was a Collection of BillingItems. A BillingItem had three basic fields, Quantity, Price and Total. [In practice it would probably have a BillID, Description etc, but we'll get to that in a minute]. So, if I used Public Variables, my Class might look something like this:
Public Class BillingItem
Public Quantity As Integer
Public Price As Double
Public Total As Double
End Class |
No, let's say that I wanted to use this class for a given order. To use my public variables, my code would look something like this:
Private Sub Form1_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load
Dim Item1 As New BillingItem
Item1.Price = "100.25"
Item1.Quantity = "2"
Item1.Total = Item1.Price * Item1.Quantity
MessageBox.Show(Item1.Total.ToString)
End Sub
|
When I run the app, a MessageBox will appear and display "200.5" which is what we expect. So, one could argue that this approach 'works'. And it does. However, who has the responsibility of ensuring that the values are valid? Who has the responsibility of making sure Total is calculated? Obviously the consumer of the class does. Now, the primary argument for public variables is that they save code, after all, you can accomplish in three lines what would otherwise take about 15. But I'd like to point out that that argument is completely false. While it's true that it saves the writer of the class some code, there's no such thing as a free lunch. And the developer using the class is going to have to take care of validation and ensuring that total is calculated each time. At a minimum, if you don't include validation, this methodology will cost the consumer of this class one line of code each time he uses it b/c he'll have to compute Total. Moreover, he'll have to compute Total each time the Price or Quantity change if he's already computed total because it's not dynamic. Now, compare that with this implementation:
Public Class BillingItem
Public Quantity As Integer
Public Price As Double
Public ReadOnly Property Total() As Double
Get
Return Quantity * Price
End Get
End Property
End Class
|
This difference alone shifted the burden of calculating total off of the consumer, and ensures that Total is going to be correct whenever you check it provided Price and Quantity are valid. Moreover, it centralizes the calculation, so a tired developer couldn't accidentally change the Total computation to Item1.Price + Item1.Quantity and not realize it until an angry customer called about it.
Ok, but now what about situation where Quantity or Price were invalid? The consumer will still have to check this each time before calling total right? Not if you use your properties correctly, one line of code can handle this at the class level, so the developer doesn't have to worry about it (I'm also going to improve upon this shortly):
Public ReadOnly Property Total() As Double
Get
If Quantity <= 0 Or Price <= 0 Then
Throw New ArgumentException("Both Price and Quantity" & _
" Must be greater than 0")
End If
Return Quantity * Price
End Get
End Property |
Validation
This isn't the optimum solution, but it is a lot better than forcing the user to check the values each time so that erroneous calculations aren't entered. But this solution stinks too because it doesn't prevent the user from entering a "0" or negative value, it just catches it if Total is called. A better solution would be to validate the values at the property level, which you can't do with public variables. Our new class would look something like this:
Public Class BillingItem
Private _Quantity As Integer
Private _Price As Double
Public Property Quantity() As Integer
Get
Return _Quantity
End Get
Set(ByVal Value As Integer)
If Value <= 0 Then
Throw New ArgumentException("Quantity Must be greater than 0")
Else
_Quantity = Value
End If
End Set
End Property
Public Property Price() As Double
Get
Return _Price
End Get
Set(ByVal Value As Double)
If Value <= 0 Then
Throw New ArgumentException("Price must be greater than 0")
Else
_Price = Value
End If
End Set
End Property
Public ReadOnly Property Total() As Double
Get
'We'll leave this check in as well in case they don't set the
'values
If Quantity <= 0 Or Price <= 0 Then
Throw New ArgumentException("Both Price and Quantity" & _
" Must be greater than 0")
End If
Return Quantity * Price
End Get
End Property
End Class
|
Now, the user of our class doesn't have to worry about doing the validation on his end. He may want to to avoid an argument exception, but we know if he puts junk values in there, it won't computer which it would before. And logic errors are the worst types. Assume that we had another requirement that we need to raise a notification every time the Total changed. How would you do it with public variables? Well, the user of your class would need to remember to make a notification every time he called anything that changed the total. By now, you should see a theme here, namely that if you use public variables, you as the user of your class, or some poor other sap is going to be doing a LOT of validation code in most instances. And in all likelihood someone is going to forget something at some point which will hopefully result in an exception or if you're really unlucky, an elusive bug that causes some real damage. To implement our Notification scheme, I'm going to create a Delegate called TotalChangedEventHandler, create an event call TotalChanged and add a handler in my client code. Now, every time the TotalChange is referenced, I'll raise an event which can be trapped to notify the user.
Raising Events
I'll add these two declarations to the top of my class:
Public Event TotalChanged As TotalChangedEventHandler
Public Delegate Sub TotalChangedEventHandler() |
Add this line to the Total Property:
RaiseEvent TotalChanged()
Return Quantity * Price
|
Add this to my form's code where I declared Item1:
AddHandler Item1.TotalChanged, AddressOf NotifyUser
Private Sub NotifyUser()
MessageBox.Show("Total Changed")
End Sub |
Wow, a whopping 8 lines of code that will raise a notification any time the object's Total changes. Compare that to using public variables. If you had under 8 instances of the BillItem class, than you'd save some code, but anything above that and you'd be costing code. And if your class will only be used 8 times you probably need to rethink your object design strategy and make some more reusable objects.
Indexed Properties
Now, let's make this class a little cooler in a way that you couldn't do safely with public variables. Let's make an indexed property (one of my personal favorites). Let's say that for any given BillType instances, there could be up to 5 reference items (reference item in the business sense, wherein someone changed the bill and had to sign there name to the change). To do this, we'll first create a ReferenceItems Class:
Public Class ReferenceItem
Private _ContactName As String
Private _ContactPhone As String
Private _ReferenceDate As DateTime
Public Property ContactName() As String
Get
Return _ContactName
End Get
Set(ByVal Value As String)
_ContactName = Value
End Set
End Property
Public Property ContactPhone() As String
Get
Return _ContactPhone
End Get
Set(ByVal Value As String)
_ContactPhone = Value
End Set
End Property
Private ReadOnly Property ReferenceDate() As DateTime
Get
Return DateTime.Now
End Get
End Property
End Class
|
We used the Public modifier so it could be seen within our class and other classes that may want to use it. Next, we create an array of ReferenceItems of 5 values:
| Private _ReferenceItems(4) As ReferenceItem |
Then create our property. Since we now know how to use properties to validate our properties, we'll only allow valid indices to be passed in:
Public Property BillingReference(ByVal Index As Integer) As ReferenceItem
Get
If _ReferenceItems(Index) Is Nothing Then
Throw New IndexOutOfRangeException("Index Specified was not valid")
End If
Return _ReferenceItems(Index)
End Get
Set(ByVal Value As ReferenceItem)
_ReferenceItems(Index) = Value
End Set
End Property |
Now, if we wanted to be cool, we could use the Default modifier in front of our indexed property and then people could reference our property like this:
Default Public Property BillingReference(ByVal Index As Integer) As ReferenceItem
Item1(0).ContactName = "Bill Ryan" |
Value Types
Let's discuss one more topic before we conclude this part of the discussion. Lets discuss the use of Value and Reference types as properties, and Structs in particular. Let's say that we made ReferenceItem a Struct and didn't have the validation rules. We couldn't set the values in it:
Public Structure ReferenceItemStruct
Public ContactName As String
Public ContactPhone As String
Public ReferenceDate As DateTime
End Structure
Private _ReferenceItemStructs As ReferenceItemStruct
Public Property ReferenceItemsStructure() As ReferenceItemStruct
Get
Return _ReferenceItemStructs
End Get
Set(ByVal Value As ReferenceItemStruct)
_ReferenceItemStructs = Value
End Set
End Property
|
Then, from our class, let's see how things behave differently:
'Won't Compile "Expression is a Value and Therefore can not be the
'Target of an assignment
Item1.ReferenceItemsStructure.ContactName = "Bill Ryan"
Dim NewStruct As ReferenceItemStruct
'Will Work
NewStruct.ContactName = Item1.ReferenceItemsStructure.ContactName |
Why is this? When a property is of type Structure, the fields can't be modified directly. You can modify them from within the class, but that's it. As Paul Vick so elegantly points out in his Visual Basic .NET Programming Language "This is because the property returns the value of the property directly, so changing the fileds of that value would not affect the value stored in that property."
Anyway, I think I've made a pretty good case on using properties vs. public variables and how to take advantage of them. We've learned that you can raise events with properties, validate values within the class, calculate values without user intervention and use indexed properties. And if you look at the amount of code needed to implement properties compared to what you save users of your class, I think the superstition of code efficiency with public variables is unquestionably wrong.