o learn how to do something well, following in the footsteps of those that have successfully done it before can be a useful practice. Think of the age old master/apprentice system, which works well and is still used today. In medicine, for example, the chief resident can be considered a master and the intern the apprentice. This principal is especially effective—yet consistently ignored—in software development. Many developers, working in isolation with little or no budget for books, are forced to recreate what has already been created and re-learn what is already known.
Fortunately, masters are available to those programmers who will apprentice themselves. Patterns and anti-patterns are a body of work that can play the role of master. Patterns are general blueprints for solutions to known problems. These blueprints, when implemented correctly and applied to the correct kind of problem, have proved effective. Anti-patterns are the opposite: solutions that are known to fail.
Patterns currently come in three flavors: creational, structural, and behavioral:
- Creational patterns deal with object creation.
- Structural patterns deal with class and object composition.
- Behavioral patterns address object interaction and the distribution of responsibility.
Because patterns are evolving and still being discovered, very few are experts in them. However, the number of pattern practitioners is increasing, and several excellent references on patterns are available today. (A must-have for every practitioner is Design Patterns: Elements of Reusable Object-Oriented Software by Erich Gamma, et. al.) In fact, many patterns—and more recently anti-patterns—are well documented, and you can easily find many successful examples.
This article discusses a pattern that is both easy to implement and easy to use in Visual Basic 6, the State pattern. VB6 can be prone to disorganization and the State pattern, a behavioral pattern, is a consummate organizer.
Dynamic Object Reclassification
Typically, problems arise when a form tries to do too much. For example, suppose you can interact with a form in the following states: browse mode, edit mode, and administrative mode. Generally, you end up writing a lot of conditional code that depends on some flag—let’s say a mode flag for argument’s sake—and uses a lot of conditional checks to turn controls on and off and permit editing and updating based on the state of this flag. The result is generally spaghetti code of varying disorganization.
Now, I am not impugning anybody’s dedication or desire. What I am saying is that requirements typically evolve and change, and complexity insidiously winds its way into code. Proactively using the State behavior pattern can prevent behavioral complexity, and reactively using it can eliminate such complexity once it rears its ugly head.
The basic idea is that a particular class represents a context. A good example is a form. The form is coded in such a way that a state object, an interface, defines its behavior. A behavior class implements the interface for each state possibility. By dynamically changing the instance of the state object, one changes the behavior of the context—in this example, the behavior of the form. This is referred to as dynamic reclassification.
Delegate Form Behavior to a State Object
You can use the State behavior pattern with a context of any complexity, but I wouldn’t necessarily employ it for every form. In fact, as a rule I wouldn’t employ it for contexts with very low complexity. However, it is easier to demonstrate with a simple example and the implementation principle doesn’t change as size increases.
|Figure 1. Simple Customer Form
In this demo, a simple customer form accepts a name and an email address (see Figure 1). The form has two modes: edit and browse. In edit mode, you can modify the value of the controls and update and delete records. In browse mode, you can only view a record. The controls are all disabled. Typically, you might use a flag to manage mode, and all of the code would exist in the form itself. In this example, you delegate the behavior of the form to a state object.
As shown in Listing 1, the form refers to an instance of a class named BaseState. The BaseState object is an instance of either EditState or BrowseState. The precise behavior is dependent on the specific instance of a class that implements BaseState. All that the form has to do (only when the state changes) is determine which state the form is in and create that state object.
Listing 1: The Form's code is implemented in terms of a state object.
Option ExplicitPrivate State As BaseStatePublic Property Get Customer() As Customer Dim c As Customer Set c = New Customer c.CompanyName = TextName.Text c.Email = TextEmail.Text Set Customer = cEnd PropertyPrivate Sub CommandDelete_Click() State.DeleteEnd SubPrivate Sub CommandUpdate_Click() State.SaveChangesEnd SubPrivate Sub Form_Load() TextName.Text = "Paul Kimmel" TextEmail.Text = "[email protected]" Set State = New EditState Call State.Initialize(Me)End SubFriend Property Get Email() As TextBox Set Email = TextEmailEnd PropertyFriend Property Get AName() As TextBox Set AName = TextNameEnd Property
Listings 2, 3, and 4 show BaseState, EditState, and BrowseState, respectively.
Listing 2: The BaseState interface
Option ExplicitPublic Sub Initialize(ByVal Form As Form1) End SubPublic Sub SaveChanges()End SubPublic Sub Delete()End SubPublic Property Let AName(ByVal value As String) End PropertyPublic Property Get AName() As String End PropertyPublic Property Let Email(ByVal value As String) End PropertyPublic Property Get Email() As String End Property
Listing 3: The EditState class implements BaseState and "turns" editing capability on.
Implements BaseState Private f As Form1 Private Sub BaseState_Delete() Debug.Assert f Is Nothing = False If (f Is Nothing) Then Return Dim c As Customer Set c = f.Customer Debug.Print "Customer: " & c.CompanyName & " deleted here"End Sub Private Property Get BaseState_Email() As String If (f Is Nothing = False) Then BaseState_Email = f.Email.Text Else BaseState_Email = "" End IfEnd Property Private Property Let BaseState_Email(ByVal RHS As String) If (f Is Nothing = False) Then f.Email.Text = RHS End IfEnd Property Private Sub BaseState_Initialize(ByVal Form As Form1) Set f = FormEnd Sub Private Property Let BaseState_AName(ByVal RHS As String) If (f Is Nothing = False) Then f.AName.Text = RHS End IfEnd Property Private Property Get BaseState_AName() As String If (f Is Nothing = False) Then BaseState_AName = f.AName.Text Else BaseState_AName = "" End IfEnd Property Private Sub BaseState_SaveChanges() Debug.Assert f Is Nothing = False If (f Is Nothing) Then Return Dim c As Customer Set c = f.Customer Debug.Print "Customer: " & c.CompanyName & " saved here"End Sub
Listing 4: The BrowseState class implements BaseState but "turns" editing capability off.
Implements BaseStatePrivate f As Form1Private Sub BaseState_Delete()End SubPrivate Property Get BaseState_Email() As String If (f Is Nothing = False) Then BaseState_Email = f.Email.Text Else BaseState_Email = "" End IfEnd PropertyPrivate Property Let BaseState_Email(ByVal RHS As String) End PropertyPrivate Sub BaseState_Initialize(ByVal Form As Form1) Set f = Form f.CommandSave.Enabled = False f.TextEmail.Enabled = False f.TextName.Enabled = FalseEnd SubPrivate Property Let BaseState_AName(ByVal RHS As String) End PropertyPrivate Property Get BaseState_AName() As String If (f Is Nothing) Then BaseState_YourName = f.AName.Text End IfEnd PropertyPrivate Sub BaseState_SaveChanges() End Sub
|Figure 2. UML Visualization of Relationship Between the Form and State Classes
Because VB6 classes are interfaces, you have to implement every member of BaseState. You also have to do this because Form1 sends the same message regardless of state. However, it is the implementation of specific properties and methods, depending on state, that determines what happens. A useful feature of the State pattern is that you can change the behavior of the form by defining new states. (Figure 2 presents a UML visualization of the relationship between the Form and state classes.)
Bare in mind that .NET supports dynamic assembly loading. This ultimately means you can load state behavioral classes dynamically and can easily add new states without recompiling a deployed application by using a .config file to indicate which states are present or should be loaded and by deploying new state assemblies only. This level of extensibility is probably impossible if not very difficult in VB6.
Manage Code Complexity
Patterns are architectural strategies that describe solutions for known kinds of problems. This article discussed the State pattern, which is a behavior pattern. The State behavior pattern supports dynamic reclassification, which is changing the behavior of a context (like a form) at runtime.
To implement the State pattern in VB6, define an interface that defines all of the properties and behaviors of your context. Then implement a specific instance of the state interface for each behavior that the context—a form in this example—supports. Add more state classes to support additional states. Each state’s code is independent of the others.
Using state objects is referred to as behavior delegation. Behavior delegation can be used proactively to prevent spaghetti code or reactively if code grows beyond a manageable level of complexity.