Saturday, December 26, 2009

Properties that return objects

Visual Basic’s own objects might expose properties that return object values. For example, forms and all visible controls expose a Font property, which in turn returns a Font object. You realize that this is a special case because you can append a dot to the property name and have IntelliSense tell you the names of the properties of the object:
Form1.Font.Bold = True
What Visual Basic does with its own objects can also be done with your custom classes. This adds a great number of possibilities to your object-oriented programs. For example, your CPerson class still lacks an Address property, so it’s time to add it. In most cases, a single Address string doesn’t suffice to point exactly to where a person lives, and you usually need several pieces of related information. Instead of adding multiple properties to the CPerson object, create a new CAddress class:
‘ The CAddress class module
Public Street As String
Public City As String
Public State As String
Public Zip As String
Public Country As String
Public Phone As String
Const Country_DEF = "USA" ‘ A default for Country property
Private Sub Class_Initialize()
Country = Country_DEF
End Sub
Friend Sub Init(Street As String, City As String, State As String, _
Zip As String, Optional Country As Variant, Optional Phone As Variant)
Me.Street = Street
Me.City = City
Me.State = State
Me.Zip = Zip
If Not IsMissing(Country) Then Me.Country = Country
If Not IsMissing(Phone) Then Me.Phone = Phone
End Sub
Property Get CompleteAddress() As String
CompleteAddress = Street & vbCrLf & City & ", " & State & " " & Zip _
& IIf(Country <> Country_DEF, Country, "")
End Property
For the sake of simplicity, all properties have been declared Public items, so this class isn’t particularly robust. In a real-world example, a lot of nice things could be done to make this class a great piece of code, such as checking that the City, State, and Zip properties are compatible with one another. (You probably need a lookup search against a database for this.) You could even automatically provide an area code for the Phone property. I gladly leave these enhancements as an exercise to readers. For now, let’s focus on how you can exploit this new class together with CPerson. Adding a new HomeAddress property to our CPerson class requires just one line of code in the declaration section of the module:
‘ In the CPerson class module
Public HomeAddress As CAddress
Now you can create a CAddress object, initialize its properties, and then assign it to the HomeAddress property just created. Thanks to the Init pseudo-constructor, you can considerably reduce the amount of code that’s actually needed in the client:
Dim addr As CAddress
Set addr = New CAddress
addr.Init "1234 North Rd", "Los Angeles", "CA", "92405"
Set pers.HomeAddress = addr
While this approach is perfectly functional and logically correct, it’s somehow unnatural. The problem stems from having to explicitly create a CAddress object before assigning it to the HomeAddress property. Why not work directly with the HomeAddress property?
Set pers.HomeAddress = New CAddress
pers.HomeAddress.Init "1234 North Rd", "Los Angeles", "CA", "92405"
When you work with nested object properties, you’ll like the With...End With clause:
With pers.HomeAddress
.Street = "1234 North Rd"
.City = "Los Angeles"
‘ etc.
End With
As I showed you previously, you can provide an independent constructor method in a standard BAS module (not shown here) and do without a separate Set statement:
Set pers.HomeAddress = New_CAddress("1234 North Rd", "Los Angeles", _
"CA", "92405")
Property Set procedures
A minor problem that you have to face is the lack of control over what can be assigned to the HomeAddress property. How can you be sure that no program will compromise the robustness of your CPerson object by assigning an incomplete or invalid CAddress object to the HomeAddress property? And what if you need to make the HomeAddress property read-only?
As you see, these are the same issues that you faced when working with regular, nonobject properties, which you resolved thanks to Property Get and Property Let procedures. So it shouldn’t surprise you to learn that you can do the same with object properties. The only difference is that you use a third type of property procedure, the Property Set procedure, instead of the Property Let procedure:
Dim m_HomeAddress As CAddress ‘ A module-level private variable.

Property Get HomeAddress() As CAddress
Set HomeAddress = m_HomeAddress
End Property
Property Set HomeAddress(ByVal newValue As CAddress)
Set m_HomeAddress = newValue
End Property
Because you’re dealing with object references, you must use the Set keyword in both procedures. A simple way to ensure that the CAddress object being assigned to the HomeAddress property is valid is to try out its Init method with all the required properties:
Property Set HomeAddress(ByVal newValue As CAddress)
With newValue
.Init .Street, .City, .State, .Zip
End With
‘ Do the assignment only if the above didn’t raise an error.
Set m_HomeAddress = newValue
End Property
Unfortunately, protecting an object property from invalid assignments isn’t as simple as it appears. If the innermost class—CAddress in this case—doesn’t protect itself in a robust way, the outermost class can do little or nothing. To explain why, just trace this apparently innocent statement:
pers.HomeAddress.Street = "" ‘ An invalid assignment raises no error.
At first, you might be surprised to see that execution doesn’t flow through the Property Set HomeAddress procedure; instead, it goes through the Property Get HomeAddress procedure, which seems nonsensical because we are assigning a value, not reading it. But if we look at the code from a compiler’s standpoint, things are different. The language parser scans the line from left to right: it first finds a reference to a property exposed by the CPerson class (that is, pers.HomeAddress) and tries to resolve it to determine what it’s pointing to. For this reason, it has to evaluate the corresponding Property Get procedure. The result is that you can’t effectively use the Property Get HomeAddress procedure to protect the CPerson class module from invalid addresses: you must protect the CAddress dependent class itself. In a sense, this is only fair because each class should be responsible for itself.
Let’s see how you can use the CAddress class to improve the CPerson class even further. You have already used it for the HomeAddress property, but there are other possible applications:
‘ In the declaration section of CPerson
Private m_WorkAddress As CAddress
Private m_VacationAddress As CAddress
‘ Corresponding Property Get/Set are omitted here....
It’s apparent that you have achieved a lot of functionality with minimal effort. Not only have you dramatically reduced the amount of code in the CPerson class (you need only three pairs of Property Get/Set procedures), you also simplified its structure because you don’t have a large number of similar properties with confusing names (HomeAddressStreet, WorkAddressStreet, and so on). But above all, you have the logic for the CAddress entity in one single place, and it has been automatically propagated elsewhere in the application, without your having to set up distinct validation rules for each distinct type of address property. Once you have assigned all the correct addresses, see how easy it is to display all of them:
On Error Resume Next
‘ The error handler skips unassigned (Nothing) properties.
Print "Home: " & pers.HomeAddress.CompleteAddress
Print "Work: " & pers.WorkAddress.CompleteAddress
Print "Vacation: " & pers.VacationAddress.CompleteAddress

No comments:

Post a Comment