Saturday, December 26, 2009

Advanced Uses of Properties

I want to tell you more about properties that can make your classes even more useful and powerful.
Enumerated properties
Some properties are intended to return a well-defined subset of integer numbers. For example, you could implement a MaritalStatus property that can be assigned the values 1 (NotMarried), 2 (Married), 3 (Divorced), and 4 (Widowed). The best solution possible under Visual Basic 4 was to define a group of constants and consistently use them in the code both inside and outside the class. This practice, however, forced the developer to put the CONST directives in a separate BAS module, which broke the self-containment of the class.
Visual Basic 5 solved this issue by adding a new Enum keyword to the VBA language and thus the ability to create enumerated values. An Enum structure is nothing but a group of related constant values that automatically take distinct values:
‘ In the declaration section of the class
Enum MaritalStatusConstants
NotMarried = 1
Married
Divorced
Widowed
End Enum
You don’t need to assign an explicit value to all the items in the Enum structure: for all the subsequent omitted values, Visual Basic just increments the preceding value. (So in the previous code, Married is assigned the value 2, Divorced is 3, and so on.) If you also omit the first value, Visual Basic starts at 0. But because 0 is the default value for any integer property when the class is created, I always prefer to stay clear of it so that I can later trap any value that hasn’t been initialized properly.
After you define an Enum structure, you can create a Public property of the corresponding type:
Private m_MaritalStatus As MaritalStatusConstants

Property Get MaritalStatus() As MaritalStatusConstants
MaritalStatus = m_MaritalStatus
End Property
Property Let MaritalStatus(ByVal newValue As MaritalStatusConstants)
‘ Refuse invalid assignments. (Assumes that 0 is always invalid.)
If newValue <= 0 Or newValue > Widowed Then Err.Raise 5
m_MaritalStatus = newValue
End Property
The benefit of using enumerated properties becomes apparent when you write code that uses them. In fact, thanks to IntelliSense, as soon as you press the equal sign key (or use any other math or Boolean operator, for that matter), the Visual Basic editor drops down a list of all the available constants, as you can see in the Figure. Moreover, all the Enums you define immediately appear in the Object Browser, so you can check the actual value of each individual item there.






Here are a few details you must account for when dealing with Enum values:
• All variables and arguments are internally managed as Longs. As far as Visual Basic is concerned, they are Longs and their symbolic names are just a convenience for the programmer.
• For the same reason, you can assign an enumerated variable or property any valid 32-bit integer value without raising an error. If you want to enforce a better validation, you must explicitly validate the input data in all your Property Let procedures, as you do with any other property.
• Enum structures aren’t exclusively used with properties. In fact, you can also create methods that accept enumerated values as one of their arguments or that return enumerated values.
• Enum blocks can be Public or Private to a class, but it rarely makes sense to create a Private Enum because it couldn’t be used for any argument or return value of a Public property or method. If the class is itself Public—in an ActiveX EXE or DLL project, for example—programmers who use the class can browse all the public Enums in the class using a standard Object Browser.
• It isn’t essential that the Enum block be physically located in the same class module that uses it. For example, a class module can include an Enum used by other classes. But if you’re planning to make your class Public (see previous point), it’s important that all the Enums that it uses are defined in other Public classes. If you put them in a Private module or a standard BAS module, you’ll get a compile error when you run the application, which you can see in the Figure.




You can’t use Enum values in a Public class if the Enum block
is located in a form module, in a Private class, or in a standard BAS module.

• Never forget that Enums are just shortcuts for creating constants. This means that all the enumerated constants defined within an Enum block should have unique names in their scope. (Because Enums are typically Public structures, their scope is often the entire application.)
The last point is especially important, and I strongly advise you to devise a method for generating unique names for all your enumerated constants. If you fail to do that, the compiler refuses to compile your application and raises the "Ambiguous name detected: " error. The easy way to avoid this problem is to add to all the enumerated constants a unique 2- or 3-letter prefix, for example:
Enum SexConstants
sxMale = 1
sxFemale
End Enum
The other way to avoid trouble is to use the complete enumname.constantname syntax whenever you refer to an ambiguous Enum member, as in the following code:
pers.MaritalStatus = MaritalStatusConstants.Married
Enum values don’t need to be in an increasing sequence. In fact, you can provide special values out of the expected order to signal some special conditions, as in the following code:
‘ In a hypothetical Order class
Enum OrderStatusConstants
osShipping = 1
osBackOrder
osError = -9999 ‘ Tip: use negative values for such special cases.
End Enum
Another common example of enumerated properties whose values aren’t in sequence are bit-fielded properties, as in this code:
Enum FileAttributeConstants
Normal = 0 ‘ Actually means "no bit set"
ReadOnly = 1 ‘ Bit 0
Hidden = 2 ‘ Bit 1
System = 4 ‘ Bit 2
Directory = 16 ‘ Bit 3
Archive = 32 ‘ Bit 4
End Enum
While enumerated properties are very useful and permit you to store some descriptive information in just 4 bytes of memory, you shouldn’t forget that sooner or later you’ll have to extract and interpret this information and sometimes even show it to your users. For this reason, I often add to my classes a read-only property that returns the textual description of an enumerated property:
Property Get MaritalStatusDescr() As String
Select Case m_MaritalStatus
Case NotMarried: MaritalStatusDescr = "NotMarried"
Case Married: MaritalStatusDescr = "Married"
Case Divorced: MaritalStatusDescr = "Divorced"
Case Widowed
If Sex = Male Then ‘ Be precise for your users.
MaritalStatusDescr = "Widower"
ElseIf Sex = Female Then
MaritalStatusDescr = "Widow"
End If
Case Else
Err.Raise 5 ‘ Defensive programming!
End Select
End Property
It seems a lot of work for such a little piece of information, but you’ll be glad that you did it every time you have to show the information on screen or in a printed report. You might wonder why I added a Case Else block (shown in boldface). After all, the m_MaritalStatus variable can’t be assigned a value outside its range because it’s protected by the Property Let MaritalStatus procedure, right? But you should never forget that a class is often an evolving entity, and what’s true today might change tomorrow. All the validation code that you use for testing the valid range of such properties might become obsolete without your even noticing it. For example, what happens if you later add a fifth MaritalStatus constant? Are you really going to hunt through your code for possible bugs each and every time you add a new enumerated value? Explicitly testing all the values in a Select Case block and rejecting those that fall through the Case Else clause is a form of defensive programming that you should always exercise if you don’t want to spend more time debugging the code later.
Here’s an easy trick that lets you safely add new constants without also modifying the validation code in the corresponding Property Let procedure. Instead of testing against the largest constant, just define it explicitly in the Enum structure:
Enum MaritalStatusConstants
NotMarried = 1
Married
Divorced
Widowed
MARITALSTATUS_MAX = Widowed ‘ Uppercase is easier to spot.
End Enum

Property Let MaritalStatus(ByVal newValue As MaritalStatusConstants)
‘ Refuse invalid assignments. (Assumes that 0 is always invalid.)
If newValue <= 0 Or newValue > MARITALSTATUS_MAX Then Err.Raise 5
m_MaritalStatus = newValue
End Property
When you then append constants to the Enum block, you need to make the MARITALSTATUS_MAX item point to the new highest value. If you add a comment, as in the preceding code, you can hardly miss it.

No comments:

Post a Comment