Saturday, December 26, 2009

Property procedures

Let’s go back to the CPerson class and see how the class can protect itself from invalid assignments, such as an empty string for its FirstName or LastName properties. To achieve this goal, you have to change the internal implementation of the class module because in its present form you have no means of trapping the assignment operation. What you have to do is transform those values into Private members and encapsulate them in pairs of Property procedures. This example shows the code for Property Get and Let FirstName procedures, and the code for LastName is similar.
‘ Private member variables
Private m_FirstName As String
Private m_LastName As String

‘ Note that all Property procedures are Public by default.
Property Get FirstName() As String
‘ Simply return the current value of the member variable.
FirstName = m_FirstName
End Property

Property Let FirstName(ByVal newValue As String)
‘ Raise an error if an invalid assignment is attempted.
If newValue = "" Then Err.Raise 5 ‘ Invalid procedure argument
‘ Else store in the Private member variable.
m_FirstName = newValue
End Property
Note You can save some typing using the Add Procedure command from the Tools menu, which creates for you the templates for Property Get and Let procedures. But you should then edit the result because all properties created in this way are of type Variant.
Add this code and write your own procedures for handling the LastName; then run the program, and you’ll see that everything works as before. What you have done, however, is make the class a bit more robust because it now refuses to assign invalid values to its properties. To see what I mean, just try this command:
pers.Name = "" ‘ Raises error "Invalid procedure call or argument"
If you trace the program by pressing F8 to advance through individual statements, you’ll understand what those two Property procedures actually do. Each time you assign a new value to a property, Visual Basic checks whether there’s an associated Property Let procedure and passes it the new value. If your code can’t validate the new value, it raises an error and throws the execution back to the caller. Otherwise, the execution proceeds by assigning the value to the private variable m_FirstName. I like to use the m_ prefix to keep the property name and the corresponding private member variable in sync, but this is just another personal preference; feel free to use it or to create your own rules. When the caller code requests the value of the property, Visual Basic executes the corresponding Property Get procedure, which (in this case) simply returns the value of the Private variable. The type expected by the Property Let procedure must match the type of the value returned by the Property Get procedure. In fact, as far as Visual Basic is concerned, the type of the property is the returned type of the Property Get procedure.
It isn’t always clear what validating a property value really means. Some properties can’t be validated without your also considering what happens outside the class. For example, you can’t easily validate a product name without accessing a database of products. To keep things simpler, add a new BirthDate property and validate it in a reasonable way:
Private m_BirthDate As Date

Property Get BirthDate() As Date
BirthDate = m_BirthDate
End Property
Property Let BirthDate(ByVal newValue As Date)
If newValue >= Now Then Err.Raise 1001, , "Future Birth Date !"
m_BirthDate = newValue
End Property
Methods
A class module can also include Sub and Function procedures, which are collectively known as methods of the class. As in other types of modules, the only difference between a Function method and a Sub is that a Function method returns a value, whereas a Sub method doesn’t. Since Visual Basic lets you invoke a function and discard its return value, I usually prefer to create Function methods that return a secondary value: This practice adds value to the procedure without getting in the way when the user of the class doesn’t actually need the return value.
What methods could be useful in this simple CPerson class? When you start dealing with records for many people, you could easily find yourself printing their complete names over and over. So you might want to devise a way to print a full name quickly and simply. The procedural way of thinking that solves this simple task would suggest that you create a function in a global BAS module:
‘ In a BAS module
Function CompleteName(pers As CPerson) As String
CompleteName = pers.FirstName & " " & pers.LastName
End Function
While this code works, it isn’t the most elegant way to perform the task. In fact, the complete name concept is internal to the class, so you’re missing an opportunity to make the class smarter and easier to use. Besides, you’re also making it difficult to reuse the class itself because you now have scattered its intelligence all over your application. The best approach is to add a new method to the CPerson class itself:
‘ In the CPerson class
Function CompleteName() As String
CompleteName = FirstName & " " & LastName
End Function

‘ In the form module, you can now execute the method.
Print pers.CompleteName ‘ Prints "John Smith"
While you’re within the class module, you don’t need the dot syntax to refer to the properties of the current instance. On the other hand, if you’re within the class and you refer to a Public name for a property (FirstName) instead of the corresponding Private member variable (m_FirstName), Visual Basic executes the Property Get procedure as if the property were referenced from outside the class. This is perfectly normal, and it’s even desirable. In fact, you should always try to adhere to the following rule: Reference private member variables in a class only from the corresponding Property Let/Get procedures. If you later modify the internal implementation of the property, you’ll have to modify only a small portion of the code in the class module. Sometimes you can’t avoid substantial code modifications, but you should do your best to apply this rule as often as you can. Once you understand the mechanism, you can add much intelligence to your class, as in the following code:
Function ReverseName() As String
ReverseName = LastName & ", " & FirstName
End Function
Remember that you’re just adding code and that no additional memory will be used at run time to store the values of complete and reversed names.
The more intelligence you add to your class, the happier the programmer who uses this class (yourself, in most cases) will be. One of the great things about classes is that all the methods and properties you add to them are immediately visible in the Object Browser, together with their complete syntax. If you carefully select the names of your properties and methods, picking the right procedure for each different task becomes almost fun.

No comments:

Post a Comment