|
Using Scope, Instancing and Enumerations for Better OOP
By by Jeffrey Hasan (Client Server Specialists, Inc.) |
Introduction
As developers, most of us understand the importance of scope, which is the "visibility, or "accessibility" that we choose to give to our procedures, variables and constants. Scope is an oft-neglected and surprisingly misunderstood concept for some developers, because they have not thought through what the distinction really implies. At a simple level, scope is distinguished between Public and Private, but the distinction is much more subtle. When leveraged properly, scope is a very powerful tool, because it provides for code that is properly encapsulated. By this, I mean that the procedures, variables and constants in a component should only be made accessible to the parts of your application that need them. A careless use of Public scope for a variable, for example, could have the unintended consequence of exposing the variable to change by parts of the application that should not even be able to access the variable. The issue of scope extends to a discussion of Instancing, which is a class module property setting that defines how objects may be created from your classes. Just as with variables, class modules restrict who sees them and how they may be used. Instancing is critical for setting up object models, and for keeping your code properly encapsulated. The purpose of this article is to examine the issues of scope and instancing, and in particular, to focus on the use of Global scope, and how you can leverage it to your advantage.Scope in Variables and Procedures
Remember that scope applies not just to variables, but also to procedures and constants, though we’ll start by examining variable scope. The most restricted scope for a variable is when it is private to a procedure, using the Dim keyword. . Please open up a new Standard EXE project, and save it as "MyEXE.vbp", then add a new Module, and set its name to Main. Switch to Form1 and type the following code:Sub Financial() Dim sngIntRate As Single End Sub Sub Analyze() End SubThe variable for interest rate, sngIntRate will only be available to the Financial procedure, and not to the Analyze procedure. If, however, you want the variable to be visible to all procedures in Form1, you would type:Private sngIntRate As Single Sub Financial() End Sub Sub Analyze() End SubSince "sngIntRate" has private scope, it will only be available for use in this code module. Outside modules in the same project can use the variable if it is scoped as:Public sngIntRate As Single
Variables should only be scoped Public in one standard module in your project. You should get in the habit of adding a declarations module to your project, which is a standard .BAS module that exclusively holds all the public variable declarations. This is good coding style, because it maintains all the public variables in one place. In our case, Main acts as the declarations module, so this is where the above variable declaration would go.When it comes to procedures, similar scoping rules apply. Assume that we have added a command button to the form, called "cmdNext", which we want to use to call Financial. Since the form module holds Financial, you can declare it as Private, however, for Financial to be accessible from outside the form module, you must scope it as Public, as in:
Public Sub Financial() End Sub Private Sub Analyze() End SubThen you can call Financial from the command button, using:Private Sub cmdNext_Click() Call Financial End SubNote that because Analyze is scoped Private, it could be called from Financial but not outside the form module. Although our example shows procedures, bear in mind that the same scoping rules also apply to functions, such that:Public Function Calculate() End Subwould be visible outside its module.Scope in Constants
Constants improve code readability and maintenance by assigning names to commonly used numbers or strings. Constants may be declared as any data type, for example:Const INTRATE As Single = 5.5 Const MYNAME as String = "Jeffrey Hasan"Public constants are available to the entire project, and should be declared in a declarations (.BAS) module. Private constants should be declared in the modules that use them. Constants may not be declared in procedures using the Dim keyword, because this defeats the purpose of using a constant.Public Const INTRATE As Single = 5.5is an example of how to declare a public constant. In our example project, this declaration should go only in Main. Later, we will discuss how to encapsulate constants in an enumeration, which exposes constants even further than with the Public keyword.Scope in Class Modules
Class modules define a blueprint for objects that can be created from the class. Objects expose an interface that allows an outside project to access the components properties and functions. The interface is defined by the scope that you give these properties and procedures. Public property procedures in a class module define properties of the object, while Public procedures are exposed as methods. Objects may also hold procedures for internal use only, so these procedures must be scoped Private.Before proceeding, please add a new ActiveX DLL project to the project group, and save it as "MyDLL.vbp". Add a new class module to your project and name it clsFinance. It is very important not to skip this step! Your project viewer should now show the following:
Figure 1Make sure you set a reference to "MyDLL.vbp" from "MyEXE.vbp", using the References dialog box. Next, add the following code to clsFinance:
Private mIntRate As Single Private mPrincipal As Single Public Property Get IntRate() As Single IntRate = mIntRate End Property Public Property Let IntRate(vdata As Single) mIntRate = vdata End Property Public Property Get Principal() As Single Principal = mPrincipal End Property Public Property Let Principal(vdata As Single) mPrincipal = vdata End Property Public Function Calculate() Calculate = mPrincipal * mIntRate End FunctionAn object created from this class will thus expose 2 properties and one method. The properties are stored internally as local, private variables, such as mIntRate for the IntRate property.Class modules provide an additional Friend keyword for all procedure types, which allows the procedure to be accessed from anywhere within the project, but not from outside the project. Think of it as a restricted form of Public. Our ActiveX DLL project should not make use of the Friend keyword, because it will be compiled as a component that must expose its properties and methods to the outside world.
Friend Function Calculate() Calculate = mPrincipal * (mIntRate/100) End Functionis an example of how to declare a function as Friend. This function could then be called from anywhere within "MyDLL.vbp", but could not be called from "MyEXE.vbp". Make sure you change the scope back to Public before proceeding!The Instancing Property
ActiveX DLL components are designed to be referenced by calling applications which need the component’s functionality. The calling application creates objects from the class modules that are contained within the component. The Instancing property settings of the class modules in the component dictate how the calling application is allowed to interact with these class modules. In our example project,"MyEXE.vbp" is the calling application, and "MyDLL.vbp" is the referenced component.By default, when you add a new class module to an ActiveX DLL, its instancing property is set to MultiUse, which means that the calling application can create several objects from the same class module. For example:
Dim c, d, e, f As New clsFinanceThis code could be placed in the Form_Load() event of Form1, for example, and would dimension 4 objects based on clsFinance. (The alternative SingleUse instancing options won’t be discussed here, because they only apply to out-of-process ActiveX EXE components).Sometimes you want your component to create objects internally, but not allow a calling application to do the same. This is what Private instancing does. On the other hand, you may want only to restrict the creation privileges, but not the use of the object. PublicNotCreatable instancing means that only your component can create an object, but once the object is created, it is available for use by the calling application.
GlobalMultiUse instancing is where things get very interesting. It is used to create general purpose procedures that are added to the global namespace of the project. These procedures appear in the Object Browser under <globals>, as if they were part of Visual Basic. In addition, these procedures can be used without dimensioning the class first using an object variable.
For example, we currently use our Calculate function by coding the following (place the code in Main and then call Analyze from the Form_Load() event):
Public Sub Analyze() Dim c As clsFinance Set c = New clsFinance c.INTRATE = 5.5 c.Principal = 5500 MsgBox "You will pay $" & c.Calculate & " in interest." Set c = Nothing End SubHowever, if we reset the instancing of clsFinance to GlobalMultiUse, then we only need to code the following:Public Sub Analyze() INTRATE = 5.5 Principal = 5500 MsgBox "You will pay $" & Calculate & " in interest." End SubOpen the Object Browser and confirm for yourself that INTRATE, Principal and Calculate all appear in bold under <globals>.Bear in mind a few things when using GlobalMultiUse instancing:
You have a wide choice of settings for the Instancing property, and so you should be aware of the implications of each setting. ActiveX DLL components are designed to provide several levels of functionality, so some class modules may be instanced differently from others. In future articles we will look at the important role that Instancing plays in constructing object models.
- The class interface will only appear in the global namespace if your current project references the GlobalMultiUse class. The interface is not permanently added to the global namespace.
- Although the class interface will appear in the global namespace, the component itself that provides the global class, cannot use the class procedures as <globals>. The class itself must reference the class in the "usual" way, using object variables.
- Be careful when choosing a name for your global interface, so as not to conflict with existing entries in <globals>. By convention, you should prefix each global entry with a 4 letter lowercase prefix that reflects the name of the class module. For example, Calculate should be renamed to cfinCalculate.
- Think carefully before setting a class modules instancing property to GlobalMultiUse. You want to avoid polluting the global namespace with unnecessary interfaces. Plus, you want to avoid creating name conflicts with existing <globals> entries.
Using Enumerations
We complete our discussion with the interesting topic of "Enumerations", which are a way of packaging several user defined constants together, usually ones that are related in some way. If we want our example class module, clsFinance, to store a reference interest rate and principal, we could add these constants in the header of the module as:Public Enum General cfinTodaysIntRate = 6 cfinTodaysPrincipal = 5500 End Enum"General" is the name of the enumeration, and the two entries are known as its members. Enumerations are added to the component’s type library, and appear in the object browser, under <globals>, even if the instancing of the class module is not set as GlobalMultiUse.Switching over to "MyEXE.vbp", we can use the enumeration as follows:
MsgBox "Today's interest rate is " & MyDLL.General.cfinTodaysRntRate
The reference to the enumerated constant is coded using the format [libname]. [enumname]. [constantname], where libname is the component name, enumname is the enumeration name, and constantname is the enumeration member. This syntax guarantees that you will reference the correct constant plus allows VB’s IDE to display the choice of constants to you in a floating drop-down box. As a shortcut, you may also use the syntax:
MsgBox "Today's interest rate is " & cfinTodaysIntRate
One of the drawbacks of enumerations is that Visual Basic currently only supports the Long datatype, so that the enumerated values must fall in the range +/- 2,147,483,648. If you want other data types to show up in the component’s type library, then you must use a workaround. Simply set the instancing of the class module to GlobalMultiUse, then add public property get procedures. These will then show up in the type library of "MyDLL", under <globals>. For example, add the following procedure to clsFinance:
Public Property Get NYSE() As String NYSE = "New York Stock Exchange" End PropertyConfirm that the instancing is set correctly, then switch over to "MyEXE.vbp". Open up the object browser and confirm that NYSE appears in bold, as a member of the type library of "MyDLL". You can then use NYSE from anywhere within "MyEXE.vbp".
Conclusions
The concepts and techniques we’ve discussed here further the goal of clear, elegant coding, and provide a good set of tools for object oriented programming. The components that you create do not exist in isolation. Instead, every line of code has the potential of being accessed outside the component. It is your responsibility to decide how the procedures may be accessed, and how they appear to the outside world. Understanding scope, instancing and enumerations helps you to do this.
About the Author
Jeffrey Hasan
Consultant
Client Server Specialists, Inc.
jhasan@cssi.org
|