Click here to Skip to main content
Click here to Skip to main content

Refactoring - elixir of youth for legacy VB code

, 3 Dec 2004
Rate this:
Please Sign up or sign in to vote.
Standard refactoring techniques coupled with automated refactoring tool provide excellent platform for legacy VB code upgrade. Legacy VB code suffers poor structure and bloated code due to lack of inheritance and other OO capabilities.

Introduction

It seems that in programming, a new paradigm rarely replaces the old one, rather it builds upon its predecessor. These days, for example, there is a lot of talk about Aspect Based Programming (AOP). It is an exciting new concept that can better express certain intentions than Object Oriented Programming (OOP) and sometimes provide better reuse. However, it is a general consensus that it’s not poised to replace the OOP.

A little back in time, since the advent of COM, VB programmers were (albeit, often unknowingly) on forefront of a new paradigm, “component based programming”. They learned how to assemble components and to treat them as binary black boxes with well-defined, self-describing interfaces. It proved to be an effective way to assemble GUI, by combining different “controls” and creating new ones, or to assemble and separate business logic inside components. It also promoted an encapsulation, since “inheritance breaks encapsulation”, and VB, while incorporating “interface” construct, had no implementation inheritance capabilities.

On the other hand, VB programmers were often treated as pariah of programming community. On Java side, they frowned at the fact that VB had no inheritance, while C++ programmers had a difficult time getting over the fact that VB had no pointers and almost no direct memory manipulation. VB was often called a “toy language”. The tool and the language also carried a lot of burden from the past, like possibility to use global functions and data, sub-procedure control statements and syntax that was rarely to anyone’s liking, just to name a few. Even the border between tool and the language was rather blurred.

While it is possible to write good code in VB, the tool’s RAD nature led to code where quality is rarely taken into consideration. It is easy to write code where lines proliferate, but structure and reuse are often lacking. For certain small projects, the visual development is remarkably effective. On the other hand, with VB entry into enterprise, especially with MTS components or COM+ and Microsoft’s distributed “DNA” architecture, and evolution from client-server, developing robust software with VB became increasingly difficult. Still, VB has won a role in the enterprise, and it is a popular tool for proprietary software development.

These days, VB.NET has most of the characteristics of modern OO languages. It has been a major shift from the previous version and a new direction in sense of programming paradigm, in a certain way marking a return to basic OO principles. While components are still there, VB now boasts inheritance.

A lot of enterprises are now in a situation where they will need to maintain legacy VB code-base along with one newly developed in VB.NET. It is easy to imagine why this fact implies additional cost for enterprise. Development teams need to maintain double set of skills. Interoperability and reuse, while possible, is difficult, and all benefits of new platform are not available to legacy programmers. The old tool will lack a manufacturer support. This can unquestionably be a turning point for an enterprise, since outdated information technology can haunt business and limit its capacity to respond quickly to new challenges. For example, thanks to the underlying platform, VB.NET is “web service ready” while productivity of VB6 in this area is much more limited. And while differences between VB.NET and prior VB versions are significant, there is still a good base for upgrade and a decent tool support for this purpose.

In this article, we will take a look now at some of the common VB 6 idioms. We will use a popular VB 6 “Engine Collection Class” pattern. Article is available here and source code available for download at this link. We'll try to illustrate how we can migrate code while trying to reap the benefits of the new language capabilities. We will see that common refactoring transformations provide an excellent base for upgrade.

Please note that we will not dwell on language and syntax details, since there are tools and guides that can lead through this process. Suffice to say, we need to get our VB 6 code working inside VB.NET IDE with bare minimum of changes applied. We advise making most of the changes with VB.NET because of stricter language and better testing framework support. We can make use of “Code Advisor”, a free tool from Microsoft that works as a plug-in for VB 6. It parses the code and makes “FixIt” comments in code, marking parts of code that will not be migrated automatically. It also permits adding custom code-checking rules.

Another detail, conditional compilation will get into the way of upgrade. So, one trick is replacing conditional compilation variables with global (module) variables and conditional compilation keywords with ordinary conditions. Replace:

#If DebugVersion  Then
    '...
#End If

with:

If DebugVersion Then
    '...
End If

And declare variable in modAccount:

Public DebugVersion As Boolean

After upgrade, we can erase global variables, and turn conditional compilation back on.

Hopefully, we have some testing harness in place that can help us make these modifications safely. If not, we can get hold of some automated testing tool and generate it. This should help us open up our project with Visual Studio .NET IDE, when upgrade will take place, smoothly.

Warming up the cauldron

Transmutation No. 1: Write Unit Tests

Before we start making any changes to our code, we need to make sure we don’t break it instead. We can use open source NUnit framework to help us with this task. NUnit is a .NET port of a rather well known unit testing framework in Java – JUnit. Working with existing code can be very challenging for test-infected programmers, since adding tests to existing code opposed to writing code and testing simultaneously imposes a lot of difficulties. One approach is finding an “inflection point”, a narrow interface to a set of classes. Once inflection point is identified, we need to cover it with a new unit test. With time, more and more code will be under testing harness, giving us confidence when changing and modifying code.

Transmutation No. 2: Replace Variant with Strong Types and deal with optional parameters

Variant is a “universal” data type in VB 6, meaning it can act as a wrapper for any type in VB 6. In ECC pattern, extensive use of Variant is advised, for reasons of flexibility when used by scripting clients. There is no such limitation in VB.NET, so we will restore strong type checking by replacing all variants with innate types where this results trivial. Things can get a bit complicated when code checks deliberately for underlying type of Variant and bases its logic upon it.

Upon upgrading our VB 6 project to VB.NET with Visual Studio .Net IDE, all Variant declarations will be automatically replaced with Object declaration, so we are better off doing this in a more controlled manner, e.g., manually.

We just got back a compiler working for us full-time; this will help us avoid nasty surprises in runtime.

VB.NET does not understand IsMissing function. If we have structured our code based on it, we need to find another way to check for optional parameters. The most simple way to solve this problem is to declare a “special value” as a default parameter value. We must be sure that the parameter under no circumstances can hold this value. For example, collection index can never have a negative value. Instead of checking for IsMissing, we will check for special value. If parameter has special value, it means optional parameter was not used. In order to make upgrade more straight-forward, we can add our own private IsMissing function to our new VBUpgrade module; this way, the original VB IsMissing function from VBA.Information module will be shadowed and our function will be executed. This step should follow the previous and that will save us any headache we could have from dealing with variants.

Here is the code snippet:

Private Const IS_MISSING_OPTIONAL_PARAM_INT As Integer = -1
'We could write similar functions for other types (for example String etc.)
Public Function IsMissing(ByVal Param As Integer) As Boolean
If Param = IS_MISSING_OPTIONAL_PARAM Then
            IsMissing = True
      Else
            IsMissing = False
      End If
End Function

'Sample function with optional parameter declaration  
Private Sub Releasing(Optional ByVal Resource As _
            Integer = IS_MISSING_OPTIONAL_PARAM)
        If IsMissing(Resource) Then
            'Parameter is missing
        Else
            'Parameter is NOT missing
        End If
End Sub

Main ingredients

Transmutation No 3: Introduce Inheritance

While it was possible to simulate inheritance in VB6 by combining interfaces and delegation, this approach is tedious and in practice rarely used. This gives us a lot of playground for types of transformations we have in mind. In VB.NET, we can use “Extract Superclass” refactoring to provide for better reuse and better design in existing code. We can see that in ECC example, all “Class” types (as in Engine-Collection-Class) share some common methods. We will start off by applying refactoring on it. For example, we will break a CAccount class into two by extracting a new superclass: ECCClass. ECCClass superclass holds all the elements that appear in all “Class” types: properties IsNew, Dirty, DeleteFlag, ClassStorage and SecurityToken. Since there is no sense in instancing ECCClass directly, we’ll mark it as abstract by using MustInherit keyword in VB.NET. All classes that use CAccount class need not be modified; our transmutation is completely transparent for them.

Code snippet:

Before:

Public Class CAccount
    Private mblnDirty As Boolean
    Private mblnIsNew As Boolean
    Private mblnClassStorage As Boolean
    Private mSecToken As String
    Private mlngAccountID As Integer
    Public Property Dirty() As Boolean'...
    Friend Property ClassStorage() As Boolean'...
    Public Property IsNew() As Boolean'...
    Public Property DeleteFlag() As Boolean'...
    Friend Property SecurityToken() As String'...
    Public Property AccountID() As Integer'...
    'Rest of business-related properties
    '...
End Class

After:

'New extracted abstract (MustInherit) super-class ECCClass
Public MustInherit Class ECCClass
    Private mblnDirty As Boolean 
    Private mblnIsNew As Boolean
    Private mblnClassStorage As Boolean 
    Private mSecToken As String
    Public Property Dirty() As Boolean'...
    Friend Property ClassStorage() As Boolean'...
    Public Property IsNew() As Boolean'...
     Public Property DeleteFlag() As Boolean'...
    Friend Property SecurityToken() As String'...
End Class

'CAccount now inherits abstract super-class
Public Class CAccount
    Inherits ECCClass

    Private mlngAccountID As Integer

    Public Property AccountID() As Integer
     'Rest of business-related properties
    '...
End Class

Transmutation No 4: Extract Class

We need to give a “Collection” a bit more of thought. Firs thing that we can notice is that it acts both as a container and as a persistence mechanism for the “Class”. These are clearly two separate responsibilities. We are better off having them apart. We can accomplish this by applying “Extract Class” transformation. We will move Update and Delete methods to a new “AccountPersistence” class. We can see that these methods actually loop over all “ECCClass” objects in the collection checking if some of them are new, dirty or marked for deletion. Once they find such an object, they issue an appropriate command to the database: INSERT, UPDATE or DELETE, and so persist these changes. Our new persistence class has no reference to a collection of objects it needs to persist, so we will add an AccountCollection field to our AccountPersistence class. This field is of ColAccount type and will be given value by AccountPersistence constructor. AccountPersistence constructor we need to add will receive single parameter, ColAccount. ColAccount, on the other hand, will inside its own constructor create an internal AccountPersistence instance, will pass itself as a parameter of AccountPersistence constructor, and set new AccountPersistence field with this value. This is the way to make original and new class keep reference of each other.

We will keep Delete and Update method declarations in our ColAccount. Only those methods are now going to dispatch all calls to AccountPersistence instance.

Now, this sounds a bit complicated, but after looking at the code sample, you’ll see that there’s not much to it.

Code snippet:

Before:

Public Class ColAccount
        Implements System.Collections.IEnumerable
    Private mcol As Collection
    Private mvarSecurityToken As String
    Public Function Add(ByVal AccountNumber As String, _
           ByVal StatusID As Short, _
           ByVal UserNumber As Integer) As Integer    
    Public Sub Clear()
    Public ReadOnly Property Count() As Integer
    Public ReadOnly Property Item(ByVal Index As Integer) As CAccount
    Friend Property SecurityToken() As String
    
    Public Function Delete(Optional ByVal Index As _
           Integer = IS_MISSING_INT) As Boolean
    
    Friend Function Load(ByVal SecurityToken As String, _
           ByVal FilledStorage As Collection) As Boolean        
    
    Public Function MarkForDelete(Optional ByVal _
           Index As Integer = IS_MISSING_INT) As Boolean
    
    Function GetEnumerator() As System.Collections.IEnumerator Implements _
                             System.Collections.IEnumerable.GetEnumerator
            GetEnumerator = mcol.GetEnumerator        
    End Function        
    
    Public Function Remove(Optional ByVal Index As _
           Integer = IS_MISSING_INT) As Boolean
        
    Public Function Undelete(Optional ByVal Index _
           As Integer = IS_MISSING_INT) As Boolean
        
    Public Function Update(Optional ByVal Index _
           As Integer = IS_MISSING_INT) As Boolean
End Class

After:

Public Class ColAccount
    Implements System.Collections.IEnumerable
    Friend mcol As Collection 'Used to store the Classes
    'reference to AccountPersistence instance we are to delegate 'methods to
    Private mPersistence As AccountPersistence
    'Constructor
    Public Sub New()
        mcol = New Collection()
        'Creates new instance of AccountPersistence
        mPersistence = New AccountPersistence(Me)
    End Sub
    
    Public Sub Clear()
    Public ReadOnly Property Count() As Integer 
    ' Returns an item from the Collection or returns Nothing.
    Public ReadOnly Property Item(ByVal Index As Integer) As CAccount
    ' Fills the Collection. Returns True or False.
    Friend Function Load(ByVal SecurityToken As String, _
           ByVal FilledStorage As Collection) As Boolean
    Function GetEnumerator() As System.Collections.IEnumerator Implements _
             System.Collections.IEnumerable.GetEnumerator
        GetEnumerator = mcol.GetEnumerator
    End Function
             
    Public Function Remove(Optional ByVal Index As _
           Integer = IS_MISSING_INT) As Boolean
    
    Public Function Add(ByVal AccountNumber As String, _
           ByVal StatusID As Short, _
           ByVal UserNumber As Integer) As Integer

    ' delegates
    Public Function Delete(Optional ByVal Index As _
           Integer = IS_MISSING_INT) As Boolean
        Delete = mPersistence.Delete(Index)
    End Function
     
    Public Function MarkForDelete(Optional ByVal Index _
           As Integer = IS_MISSING_INT) As Boolean
        
    Public Function Undelete(Optional ByVal Index As _
           Integer = IS_MISSING_INT) As Boolean
    
    ' delegates        
    Public Function Update(Optional ByVal Index As _
           Integer = IS_MISSING_INT) As Boolean
        Update = mPersistence.Update(Index)
    End Function

    ' delegates
    Friend Property SecurityToken() As String
        Get
            SecurityToken = mPersistence.SecurityToken
        End Get
        Set(ByVal Value As String)
            mPersistence.SecurityToken = Value
        End Set
    End Property
End Class
        
'New extracted class

Public Class AccountPersistence
    Private mcol As ColAccount
    Private mvarSecurityToken As String
        
    ' Constructor receives a reference to underlying collection
    Public Sub New(ByRef pcolAccount As ColAccount)
        mcol = pcolAccount
        End Sub
    Public Function Delete(Optional ByVal Index As _
           Integer = IS_MISSING_INT) As Boolean'...
    Public Function Update(Optional ByVal _
           Index As Integer = IS_MISSING_INT) As Boolean
    '...
    Friend Property SecurityToken() As String'...
End Class

No 5: Extract Method - replace optional parameter with overloaded methods

Thanks to the possibility to overload methods in VB.NET, we can make some further adjustments to our Persistence class. We can see that Update and Delete methods contain single optional parameter in the declaration. Basically, we can perform update or delete either on single Account when parameter is specified or on whole collection when every Account is inspected for new/dirty/marked for deletion flag, and accordingly saved, updated or deleted when no parameter was given in a method call. We can conveniently extract code where check for optional parameter is performed. We can then separate conditional logic into two methods. One with, and the other method without parameter. Both of them will pass call to new private Delete method with two parameters: LowerLimit and UpperLimit.

Our client code will not break since we can still call Update and Delete methods with or without parameter. Only, now we do not need to inspect parameter and condition our code. The correct method is called, thanks to overloading mechanism.

Code snippet:

Before:

    Public Function Delete(Optional ByVal Index _
           As Integer = IS_MISSING_INT) As Boolean
    Dim ErrorNum As Integer
    Dim oDALEng As IOBPDA.IOBPConnection
    Dim oCAccount As CAccount
    Dim vParameters(0, 0) As Object
    Dim inx As Integer
    Dim SPName As String
    Dim LowerLimit As Integer
    Dim UpperLimit As Integer
    If mcol.Count = 0 Then 'Nothing to Update
        Delete = True
    End If
    'If Index is supplied then delete only the supplied record
    If Not IsMissing(Index) Then
        If Index < 1 Or Index > mcol.Count Then
            Err.Raise'...
        Else
            LowerLimit = Index
            UpperLimit = Index
        End If
    Else
        LowerLimit = 1
        UpperLimit = mcol.Count
    End If
    If Not SafeCreateObject(oDALEng, OBP_DA_CONNECTION, ErrorNum) Then
        Err.Raise'...
    End If
    For inx = UpperLimit To LowerLimit Step -1
        oCAccount = mcol.Item(inx)
            If Not oCAccount.IsNew Then
                'Delete from DB If Not New
                ReDim vParameters(PARMUBOUND, 1)
                With oCAccount
                    'Fill Parameter Array
                    .ClassStorage = True
                    vParameters(PARMNAME, 0) = _
                         PARMNAMESP_ACCOUNTACCOUNTID 'Name
                    '... set rest of parameters
                    .ClassStorage = False
                End With
                If Not oDALEng.Execute(SecurityToken, _
                       SP_D_ACCOUNT, vParameters) Then
                    Err.Raise'...
                End If
            End If
            oCAccount = Nothing
            'Remove from Collection
            mcol.Remove((inx))
        Next inx
        Delete = True
        Erase vParameters
    End Function

After:

'Receives the Index parameter
Public Function Delete(ByVal Index As Integer) As Boolean

        Dim LowerLimit As Integer
        Dim UpperLimit As Integer
        If Index < 1 Or Index > mcol.Count Then
            Err.Raise'...
        Else
            LowerLimit = Index
            UpperLimit = Index
        End If
        Delete = Delete(LowerLimit, UpperLimit)
    End Function

    'Without parameter 
    Public Function Delete() As Boolean
        Dim LowerLimit As Integer
        Dim UpperLimit As Integer
        LowerLimit = 1
        UpperLimit = mcol.Count
        Delete = Delete(LowerLimit, UpperLimit)
    End Function
    
    'Extracted Delete method with lower and upper limit parameters is private
    Private Function Delete(ByRef LowerLimit As Integer, _
                                  ByRef UpperLimit As Integer)
        Dim ErrorNum As Integer
        Dim oDALEng As IOBPDA.IOBPConnection
        Dim oCAccount As CAccount
        Dim vParameters(0, 0) As Object
        Dim inx As Integer
        Dim SPName As String
        If mcol.Count = 0 Then 'Nothing to Update
            Delete = True
        End If
        If Not SafeCreateObject(oDALEng, OBP_DA_CONNECTION, ErrorNum) Then
            Err.Raise'...
        End If
        For inx = UpperLimit To LowerLimit Step -1
        ' ... code to execute delete stored procedure – set rest of parameters
        Next inx
        Delete = True
    End Function

   ' Change in ColAccount
   ' delegates
    Public Function Delete(Optional ByVal Index _
                    As Integer = IS_MISSING_INT) As Boolean
        If Not IsMissing(Index) Then
            Delete = mPersistence.Delete(Index)
        Else
            Delete = mPersistence.Delete()
        End If
    End Function

Transmutation No 6: Replace Delegation with Inheritance

In VB.NET, a standard way to implement a collection class is to extend some of the classes already provided by the .NET framework. This works well for our ColAccount class. We can inherit CollectionBase class. We don’t need to implement System.Collections.IEnumerable interface since CollectionBase already implements it. We can delete GetEnumerator, Clear and Count methods, internal mcoll Collection variable, and use default implementation from CollectionBase class our ColAccount class now extends. We can observe another characteristic of our ColAccount class. Since all persistence code was removed in transformation no. 2, our collection does not need to have knowledge of Data Access Layer. Also, in majority of methods, it can manipulate all ECCClass descendants in the same way, knowing it only by its superclass.

Code snippet:

Before:

Public Class ColAccount
    Implements System.Collections.IEnumerable

    Friend mcol As Collection 
    Private mPersistence As AccountPersistence
    Public Sub New()
        mcol = New Collection()
        mPersistence = New AccountPersistence(Me)
    End Sub
    Public Sub Clear()
        'Create new Collection
        mcol = New Collection()
    End Sub
    Public ReadOnly Property Count() As Integer
        Get
            If mcol Is Nothing Then
                Count = 0
            Else
                Count = mcol.Count()
            End If
        End Get
    End Property
    ' Returns an item from the Collection or returns Nothing.
    Public ReadOnly Property Item(ByVal Index As Integer) As CAccount
        Get
            Dim mintCodeID As Short
            If mcol Is Nothing Or mcol.Count() = 0 Then
                Exit Property
            End If            
            Item = mcol.Item(Index)
        End Get
    End Property
    ' Fills the Collection. Returns True or False.
    Friend Function Load(ByVal SecurityToken As String, _
           ByVal FilledStorage As Collection) As Boolean
        Load = False
        mPersistence.SecurityToken = SecurityToken
        mcol = FilledStorage
        Load = True
    End Function
    Public Function GetEnumerator() As System.Collections.IEnumerator 
        Implements System.Collections.IEnumerable.GetEnumerator
        GetEnumerator = mcol.GetEnumerator
    End Function
    Public Function Remove(Optional ByVal Index _
           As Integer = IS_MISSING_INT) As Boolean
        Remove = False
        If IsMissing(Index) Then
            Me.Clear()
            Remove = True
        End If
        If (Index < 1 Or Index > Me.Count) Then
            Err.Raise'...
        End If
        mcol.Remove((Index))
        Remove = True
    End Function
    Public Function Undelete    ' ...
    Public Function MarkForDelete  ' ...
    Public Function Add ' ...
    '... Functions Delete, Update, 
    'SecurityToken delegates to AccountPersistance
End Class

After:

'ColAccount now inherits CollectionBase
Public Class ColAccount
    Inherits CollectionBase

    'Reference to AccountPersistence instanca we are to delegate methods to
    Private mPersistence As AccountPersistence
    Public Sub New()
        mPersistence = New AccountPersistence(Me)
    End Sub
    ' Returns an item from the Collection or returns Nothing.
    Public ReadOnly Property Item(ByVal Index As Integer) As CAccount
        Get          
            If InnerList.Count() = 0 Then
                Exit Property
            End If
            'maintain compatibility (Underlying ArrayList 0 based)
            Item = InnerList.Item(Index - 1)
        End Get
    End Property
    ' Fills the Collection. Returns True or False.
    Friend Function Load(ByVal SecurityToken As String, _
           ByVal FilledStorage As ArrayList) As Boolean
        Dim account As CAccount
        Load = False
        mPersistence.SecurityToken = SecurityToken
        InnerList.Clear()
        For Each account In FilledStorage
            InnerList.Add(account)
        Next
        Load = True
    End Function
    ' Removes an item in the Collection if Index Key
    ' passed in or else it calls
    ' Clear. Returns True or False.
    Public Function Remove(Optional ByVal Index _
           As Integer = IS_MISSING_INT) As Boolean
        Remove = False
        If IsMissing(Index) Then
            Me.Clear()
            Remove = True
        End If
        If (Index < 1 Or Index > Me.Count) Then
            Err.Raise(ERR_ACCOUNTINDEXOUTOFRANGE, _
                      "ColAccount.Remove PROC", _
                      "Remove Failed: Index out of range.")
        End If
        InnerList.Remove((Index))
        Remove = True
    End Function

    '... Functions: MarkForDelete, Undelete, Add, Delete, Update
End Class

Transmutation No. 7: Dealing with persistence class: Extract Super

AccountPersistance class has a few quite long methods. We should reorganize them, if only to make them shorter. In this case, we follow an additional purpose, separating a more “generic” from Account type specific code. So in the same time, we will replace more specific references to CAccount with references to ECCClass.

From Delete method, we can extract PrepareDeleteData that sets deletion parameters, and we can replace calls to stored procedure name constant with StoredProcedureDelete method.

Similarly, from Update method, we can extract PrepareUpdateData, StoredProcedureInsert, StoredProcedureUpdate and UpdateCollectionWithReturnedData.

What we are actually doing in this point, is preparing “hook” methods; once we perform extract superclass, only these methods will need to be implemented.

Now we can extract Super. We can call it ECCPersistence and we will mark it as abstract (MustInherit) It will receive Add, Insert and Delete methods. It will also declare all our Account-specific methods abstract - MustOverride: PrepareDeleteData, ExecuteDelete, PrepareUpdateData, ExecuteUpdate and UpdateCollectionWithReturnedData. Our AccountPersistence class already implements these methods.

Code snippet:

Before:

Public Class AccountPersistence
    ' ...        
    Private Function Delete(ByRef LowerLimit As Integer, _
                            ByRef UpperLimit As Integer)
    Dim oDALEng As IOBPDA.IOBPConnection
        '... declare rest of variables ...
        For inx = UpperLimit To LowerLimit Step -1
            oCAccount = mcol.Item(inx)
            If Not oCAccount.IsNew Then
                'Delete from DB If Not New
                ReDim vParameters(PARMUBOUND, 1)
                With oCAccount
                    'Fill Parameter Array
                    .ClassStorage = True
                    vParameters(PARMNAME, 0) = _
                            PARMNAMESP_ACCOUNTACCOUNTID 'Name
                        '... set rest of parameters
                    .ClassStorage = False
                End With
                If Not oDALEng.Execute(SecurityToken, _
                       SP_D_ACCOUNT, vParameters) Then
                    Err.Raise'...
                End If
            End If
            oCAccount = Nothing
            'Remove from Collection
            mcol.Remove((inx))
        Next inx
        Delete = True
    End Function
' ...
End Class

After:

    Public MustInherit Class ECCPersistence
    Private mvarSecurityToken As String
    Private mcol As ColAccount
    Public Sub New(ByRef pcolAccount As ColAccount)
        mcol = pcolAccount
    End Sub
    Protected MustOverride Function StoredProcInsert() As String
    Protected MustOverride Sub UpdateCollectionWithReturnedData(ByVal _
                           rsAccount As Recordset, ByRef entity As ECCClass)
    Protected MustOverride Function StoredProcUpdate() As String
    Protected MustOverride Function PrepareUpdateData(ByVal eccentity _
                           As ECCClass, ByVal vParameters As Object) As Object
    Protected MustOverride Function PrepareDeleteData(ByVal oEntity _
                           As ECCClass, ByVal vParameters As Object) As Object
    Protected MustOverride Function StoredProcDelete() As String
    Public Function Delete() As Boolean' ...
    Public Function Delete(ByVal Index As Integer) As Boolean' ...
    Private Function Delete(ByRef LowerLimit As Integer, _
                            ByRef UpperLimit As Integer) As Object
        Dim oDALEng As IOBPDA.IOBPConnection
'... declare rest of variables
        If mcol.Count = 0 Then 'Nothing to Update
            Delete = True
        End If
        For inx = UpperLimit To LowerLimit Step -1
            oEntity = mcol.Item(inx)
            If Not oEntity.IsNew Then
                vParameters = PrepareDeleteData(oEntity, vParameters)
                If Not oDALEng.Execute(SecurityToken, _
                       StoredProcDelete, vParameters) Then
                    Err.Raise'...
                End If
            End If
            'Remove from Collection
            mcol.Remove((inx))
        Next inx
        Delete = True
    End Function
    Friend Property SecurityToken() As String' ...
    Public Function Update(ByVal Index As Integer) As Boolean' ...
    Public Function Update() As Boolean    ' ...
    Private Function Update(ByRef LowerLimit As Integer, _
                            ByRef UpperLimit As Integer)
        Dim oDALEng As IOBPDA.IOBPConnection
'... declare rest of variables ...
        For inx = LowerLimit To UpperLimit
            oEntity = mcol.Item(inx)
            If oEntity.Dirty Then
            'Extracted method hook
                vParameters = PrepareUpdateData(oEntity, vParameters)
                'Check to see if updating existing record to database
                If oEntity.IsNew = False Then
                    'Update the record - Extracted method hook
                    SPName = StoredProcUpdate()
                Else
                    'Inserting the record - Extracted method hook
                    SPName = StoredProcInsert()
                End If
                If Not oDALEng.Execute(SecurityToken, _
                       SPName, vParameters, rsAccount) Then
                    Err.Raise'...
                ElseIf (Not rsAccount Is Nothing) Then  
                    If rsAccount.RecordCount > 1 Then
                        Err.Raise'...
                    Else
             'Extracted method hook
                        UpdateCollectionWithReturnedData(rsAccount, oEntity)
                    End If
                End If
            End If 'Dirty
        Next inx
        Update = True
        Erase vParameters
    End Function
End Class

'The class implements all abstract methods – see GOF Template pattern
Public Class AccountPersistence
    Inherits ECCPersistence
    Public Sub New(ByRef pcolAccount As ColAccount)
        MyBase.New(pcolAccount)
    End Sub
    Protected Overrides Function PrepareDeleteData(ByVal oCAccount _
              As ECCClass, ByVal vParameters As Object) As Object
        Dim account As CAccount
        account = oCAccount
        'Delete from DB If Not New
        ReDim vParameters(PARMUBOUND, 1)
        With account
            .ClassStorage = True
            vParameters(PARMNAME, 0) = PARMNAMESP_ACCOUNTACCOUNTID 'Name
            '... set rest of parameters        
        End With
    End Function
    Protected Overrides Sub UpdateCollectionWithReturnedData(ByVal _
                        rsAccount As Recordset, ByRef entity As ECCClass)
        Dim account As CAccount
        account = entity
        With account
     '... set account with values from recordset
        End With
    End Sub
    Protected Overrides Function StoredProcUpdate() As String
        StoredProcUpdate = SP_U_ACCOUNT
    End Function
    Protected Overrides Function StoredProcInsert() As String
        StoredProcInsert = SP_I_ACCOUNT
    End Function
    Protected Overrides Function StoredProcDelete() As String
        StoredProcDelete = SP_D_ACCOUNT
    End Function
    Protected Overrides Function PrepareUpdateData(ByVal eccentity _
              As ECCClass, ByVal vParameters As Object) As Object
    ' ... set update parameters             ...
    End Function
End Class

Transmutation No. 8: Dealing with collection class: Introduce Constructor and extract subclass

Let’s take a look at Add method in ColAccount. Large part of a method is concerned with setting up new account instance properties with default values. We can extract this code to new SetUpDefaultValues method. This code should be executed in a moment of instance creation and it is really concerned with CAccount data. This over interest in other class’ data can be considered a “smell” in the refactoring sense of word. Additionally, VB.NET supports constructors, so we are better off moving this code to CAccount class and calling it from CAccount constructor. This is known as “Move Method” refactoring.

Since SetUpDefaultValues is called directly from constructor, we can “inline” this method and place all code inside constructor. We might have done all this in one go, and moved code directly to constructor. Advantage from doing it step by step as described, is in the fact that we can use tool to perform refactoring, and that is much safer and faster.

Now we will extract new Add method from the old one. It receives a single ECCClass parameter, thanks to overloading capability in VB.NET. It will receive a CAccount instance and it will add it to the InnerList. We will modify existing Add method code so it calls new Add method.

We can continue with identifying and defining new levels of abstraction. Since our ColAccount class already extends CollectionBase, we need to have a new approach. This time, we need to perform “Extract subclass” refactoring on our ColAccount class. First, we will rename it to ECCCollection. Now, we will replace reference to CAccount with reference to its super, ECCClass, and reference to AccountPersistence with reference to ECCPersistence wherever possible. After this, we can extract subclass, conveniently named ColAccount, that has only one method, Add. This is the only method referencing CAccount class.

After this, we need to go back to our ECCPersistence class and replace all references to ColAccount with references to ECCCollection.

If we take a look at the static structure diagram below, we can see that there is a layer of abstraction appearing, most of the classes now belonging to a certain hierarchy.

IOBPAccountEng is still sticking out from the bunch, but we will deal with it soon.

Diagram 1

Transmutation No. 9: Attributes

VB.NET supports new metadata construct – attributes. In some cases, .NET framework services can be implemented as simply as marking certain element with appropriate attribute. Serialization is one of those services. All we need to do is mark certain classes with <Serializable()> to be able to persist this class to a file, for example.

We will mark our ECCCollection and ECCClass with <Serializable()> attribute and we will erase it’s Store method that had similar, but more limited purpose. In this case, we will break compatibility and let client code be modified accordingly, since in the example, the client provided is not making use of this functionality.

Transmutation No 10: Introduce Factory Pattern

Engine class can be transformed along similar lines as a Collection and Entity class. Engine class can help us control the creation of ECCClass objects and its initial state. It also separates database query code from our business layer.

Following the usual lines, we will define an abstract (MustInherit) ECCEngine class and “move up” methods NewAccount renamed to NewClass, and GetAccount renamed to GetClass. If we inspect other modules, most of them use simple primary key, so we can use definition of GetClass method throughout our application. In case this changes, we could represent a primary key by the type on its own.

In IOBPAccountEng, we will delegate calls to new methods in super. We are “upcasting” the code, so it references classes higher up in the hierarchy. We need to defer decision on what concrete type of ECCCollection or ECCClass our Engine will be instantiating to a class that extends it, so we will define abstract CreateCollectionInstance method, by extracting into method a line that explicitly creates a ColAccount object:

oColAccount = New ColAccount

into:

oCollAccount = CreateCollectionInstance()

Method declaration looks like this:

Public MustOverride CreateCollectionInstance() As ECCCollection

This way, we provide a “hook” for an implementing class to decide on what type of object to create. Needless to say, object in question needs to extend ECCCollection.

Our IOBPAccountEng can now implement this method and return new ColAccount. We will leave Search method “as is”, since it depends on CAccount class.

Code snippet:

Before:

Public Class <CODE>IOBPAccountEng
    Public Function GetAccount(ByVal SecurityToken As String, _
      Optional ByVal AccountID As Integer = IS_MISSING_INT, _
      Optional ByVal DeletedRecords As IOBPAccountEng.DeleteRecordType _
      = DeleteRecordType.NonDeletedRecords) As ColAccount

'... prepare stored procedure parameters and add them to vParameters array
        If Not IsEmptyArray(vParameters) Then
            If Not oDALEng.Execute(SecurityToken, SP_S_ACCOUNT, _
                                      vParameters, rsAccount) Then
                Err.Raise'...
            End If
        Else
            If Not oDALEng.Execute(SecurityToken, SP_S_ACCOUNT, , rsAccount) Then
                Err.Raise'...
            End If
        End If
        oDALEng = Nothing
        oColAccount = New ColAccount()
        If (Not rsAccount Is Nothing) Then 'Set to True if nothing returned in DB
            'Update collection with returned data
            col = New ArrayList()
            Do Until rsAccount.EOF
 ' loop over recordset and add instances to a collection
                col.Add(oCAccount)
                rsAccount.MoveNext()
            Loop
        End If
        If Not oColAccount.Load(SecurityToken, col) Then
            Err.Raise'...
        End If
        'Success
        GetAccount = oColAccount
    End Function
    ' Returns an empty Collection so that new Classes can be added.
    Public Function NewAccount(ByVal SecurityToken As String) As ColAccount
        Dim oColAccount As New ColAccount
        oColAccount.SecurityToken = SecurityToken
        NewAccount = oColAccount
    End Function    
    Public Function Search('...
'...
End Class

After:

Public MustInherit Class ECCEngine
    Protected MustOverride Function CreateCollectionInstance() As ECCCollection
    Protected MustOverride Sub SetClassData(ByRef entity As ECCClass, _
                                            ByVal rsAccount As Recordset)
    Protected MustOverride Function StoredProcSelect() As String
    Protected MustOverride Function CreateClassInstance() As ECCClass

    Public Function GetECCClass(ByVal SecurityToken As String, _
      Optional ByVal ID As Integer = IS_MISSING_INT, _
      Optional ByVal DeletedRecords As DeleteRecordType = _
      DeleteRecordType.NonDeletedRecords) As ColAccount

'... prepare stored procedure parameters and add them to vParameters array

        If Not IsEmptyArray(vParameters) Then
        'Extracted method hook
            If Not oDALEng.Execute(SecurityToken, _
                   StoredProcSelect, vParameters, rs) Then
                Err.Raise'...
            End If
        Else
        'Extracted method hook
            If Not oDALEng.Execute(SecurityToken, StoredProcSelect, , rs) Then
                Err.Raise'...
            End If
        End If
      'Extracted method hook
        oCol = CreateCollectionInstance()
        If (Not rs Is Nothing) Then 'Set to True if nothing returned in DB
            'Update collection with returned data
            col = New ArrayList()
            Do Until rs.EOF
         ' loop over recordset and add instances to a collection
                col.Add(oClass)
                rs.MoveNext()
            Loop
        End If
        If Not oCol.Load(SecurityToken, col) Then
            Err.Raise'...
        End If
        'Success
        GetECCClass = oCol
        Erase vParameters
    End Function
    ' Returns an empty Collection so that new Classes can be added.
    Public Function NewClass(ByVal SecurityToken As String) As ColAccount
        Dim oECCCol As ECCCollection
      'Extracted method hook
        oECCCol = CreateCollectionInstance()
        oECCCol.SecurityToken = SecurityToken
        NewClass = oECCCol
    End Function
End Class

Public Class IOBPAccountEng
    Inherits ECCEngine
    Public Function GetAccount(ByVal SecurityToken As String, _
           Optional ByVal AccountID As Integer = IS_MISSING_INT, _
           Optional ByVal DeletedRecords As DeleteRecordType = _
           DeleteRecordType.NonDeletedRecords) As ColAccount
        GetAccount = GetECCClass(SecurityToken, AccountID, DeletedRecords)
    End Function
    Protected Overrides Sub SetClassData(ByRef entity As ECCClass, _
                                        ByVal rsAccount As Recordset)
        Dim oAccount As CAccount
        oAccount = entity
        With oAccount
            .AccountID = rsAccount.Fields(FN_ACCOUNTACCOUNTID).Value
     '... set oAccount with rest of values from recordset
        End With
    End Sub
    Public Function NewAccount(ByVal SecurityToken As String) As ColAccount
        NewAccount = NewClass(SecurityToken)
    End Function
    Protected Overrides Function CreateCollectionInstance() As ECCCollection
        Dim col As New ColAccount()
        CreateCollectionInstance = col
    End Function
    Protected Overrides Function CreateClassInstance() As ECCClass
        Dim entity As New CAccount()
        CreateClassInstance = entity
    End Function
    Protected Overrides Function StoredProcSelect() As String
        CreateCollectionInstance = col
        StoredProcSelect = SP_S_ACCOUNT
    End Function
    Public Function Search('...
'...
End Class

Transmutation No 11: Extract Interface

At this point, our “mixture” is doing quite well. Still, if you have a refined sense of smell, there is still a faint odor coming out from our code stew. And, since duplicated code is number one on list of “refactoring smells”, we definitely need to deal with it.

There is one property scattered and repeated throughout our code. It is SecurityToken property, used to provide security in our application. Without giving it too much thought, we can extract this property into new abstract (MustInherit) SecuredObject class. Most of classes that were implementing this property now will only extend the new SecuredObject class. That did the job with the duplication.

Now, thinking a bit about this property, we can see it has completely different responsibility compared to the rest of the code in each class. We can make that more explicit. We can extract new ISecured interface that will declare only one, SecurityToken property. Now, our intent is more deliberate. There is another favorable detail to it. If we need to introduce a new class, but this class needs to extend a different class, we can still do it. In VB, we can extend only a single class (there’s no multiple implementation inheritance) but we have no problem with implementing multiple interfaces. So, even if our class needs to extend another class, it can still be secured.

Code snippet:

Before:

Public MustInherit Class ECCClass
'...

Private mSecToken As String
'...
    Friend Property SecurityToken() As String
        Get
            SecurityToken = mSecToken
        End Get
        Set(ByVal Value As String)
            mSecToken = Value
        End Set
    End Property
'...
End Class

After

'New Interface
Public Interface <CODE>ISecured
    Property SecurityToken() As String
End Interface

'New Class
Public MustInherit Class SecuredObject
    Implements ISecured

    Private mSecToken As String
    Friend Property SecurityToken() As String Implements ISecured.SecurityToken
        Get
            SecurityToken = mSecToken
        End Get
        Set(ByVal Value As String)
            mSecToken = Value
        End Set
    End Property
End Class

Public MustInherit Class ECCClass
    Inherits SecuredObject
'...
End Class

Public MustInherit Class ECCCollection
    Inherits CollectionBase
Implements Isecured 'has different implementation – delegates, 
                    'it does not inherit SecuredObject class,
                    '   only implements an 'ISecured interface
'...
End Class

Public MustInherit Class ECCPersistence
    Inherits SecuredObject
'...
End Class

Pure Gold: Emerging Framework

We can now clearly see that the topmost classes in the hierarchy represent a repeatable pattern that can be equally effectively used in other projects. They help us enforce design decisions and rules on a lower level than the design pattern we started off with. Now, only a small portion of code is left to free interpretation, and we have a lot of code we can reuse, so we do not need to start from zero. Clearly, this is not just a design pattern anymore. We have a framework emerging!

Transmutation No 12: Rename

We have come far from the original code we started off with. We have modified our code enough to say that new concepts and new idioms have spawned. To make our code more understandable, we will execute another modification to our code, “Rename refactoring”. We can rename our ECCClass to Entity, our ECCCollection to EntityCollection, and our ECCEngine to Factory.

Diagram 2

Transmutation No. 13: Manage dependencies with VB Namespaces, VB and “Move class” refactoring

As a final step in building our framework, we need to separate classes belonging to our newly developed framework from classes belonging to a specific implementation. Another thing to have on mind: higher-level abstractions need not depend on those on lower level.

VB.NET introduces a concept of Namespace. In previous versions of VB, namespaces were implicit, intertwined with compilation unit (component), and had only one level. In VB.NET, namespaces are hierarchical and have no relation with source code or compilation unit.

The solution is in defining a new namespace for our framework classes. I will call it “TeslaTeam.RefactoringVB.RefactoredECCFramework”. We will perform Move Class refactoring in order to reallocate our framework classes to this new namespace. In VB.NET, we can have multiple namespaces and classes in the same source code file, but our framework classes should end up in source files separate from the rest of the application specific classes. Once we start to distribute the framework to our clients, we do not want our application classes being distributed as well!

Diagram 3

Transmutation No 14: Option Strict On

Unit tests provide a good coverage for any errors we might have made. They are our main instruments for making sure things we change don’t break our code. Still, if we want to make our process more robust, we can prevent implicit type conversions and we can switch “Option Strict On”. Then, we will need to perform all our conversions in an explicit way. This will give even more security to our code.

Seeing the future

We will now shortly describe a new feature announced for “Whidbey” version of Visual Basic, to be released some time in 2005 according to Microsoft. It can help us create more efficient and type safe code. Actually, it will eliminate a need for one class in our framework.

Future Transmutation No. 1: Refactor Collection to Generic

We can see that our ColAccount is very similar to other collection classes in the framework. However, it is difficult to isolate this common behavior in the form of inheritance because most of the differences come from a type that the Collection manipulates, and of which Collection serves as a container. We could have an implementation where all ECCClass children are treated as its common super type, but it would make us perform a lot of cast operation and would work against type safety. For example, we could place instances of CAccount and CBill inside the same container. Current version of VB cannot help us avoid this. But, Whidbey version of VB.NET boasts another important language enhancement, generics.

Generics will permit us define a Collection in a “generic” way, as a form of template that will receive its definite form at the moment it is instantiated.

We will define collection, according to the current specification, like this:

Public Class ECCColl(Of ItemType)

We will modify ECCAccountEngine class to instantiate collection:

Dim coll As New ECCCollection(Of CAccount)

We have just eliminated a need for another subclassing in our framework. This makes our framework more simple and easier to use!

A step further: Refactoring GUI transmutation

Our ECC code example also carries code that demonstrates pattern use through fully functional application that performs basic maintenance: Search, Insert new, Update or Delete ECCClass types. Since we applied behavior preserving refactoring techniques to previous parts of code, our GUI code requires no changes and it is still fully functional.

As typical VB6 code, it suffers from a lot of already described “smells” and it can benefit greatly from refactoring. What’s more, we will try to do it in such a way so we can enlarge our framework with some ready-made GUI framework solution.

ECC example is essentially a pattern for tiered architecture, that means there is loose coupling between tiers, and dependencies generally go from database to business to UI tier. While we can provide UI solution inside our framework, it will in no way limit a framework consumer to build its own and completely new UI solution while reusing the rest of our framework.

Visual components inheritance

We can see there is great resemblance in each of our CRUD forms, they all display and manipulate data in a similar way. Will start off by extracting superclass, we can call it ECCCRUDForm. This class will contain majority of methods from our frmAccount class except two: FillForm and FillCollectionFromForm, since these methods need to manipulate the CAccount class. We can still declare them as abstract and replace Aaccount parameter with ECCClass type. The subclass that will implement these methods will cast them to the appropriate ECCClass subtype.

Our ECCCrudeForm need not have any knowledge of any specific classes, it needs to reference only ones pertaining to our newly build framework. So we will replace all references to ColAccount with ECCCollection, and references to CAccount with references to ECCClass. It will also contain all data manipulation controls (Update, Cancel, Delete etc.). Our frmAccount class will contain code that adds controls needed to display CAccount data. We can see that this refactoring is easily propagated to rest of CRUD forms, providing another point for reuse in our framework.

Conclusion

We started out by trying to eliminate the most obvious shortcomings in our code. We tried to purify it, eliminating duplication and simplifying long methods and large classes. However, very soon, we saw strong abstractions appearing. With only a little guidance while refactoring, a comprehensive, more robust and extensive new design has appeared.

We have seen how even well thought-out legacy VB code lends itself easily to refactoring, and how it can be a powerful tool for upgrading our code. A change in perspective on value and usability of existing code is definitely in place.

VB is now a fully capable OO language, and major benefits are obtained from making deep changes to design and in a way of thinking, by using new paradigms and language capabilities. It may not be a philosopher's stone, but it is definitely a very useful tool in an expert VB programmer's toolbox.

Flywheel 7.2 from Velocitis - www.velocitis.com used to automate common refactoring transformations and to create static structure diagrams.

History

  • First version.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

Share

About the Author

Danijel Arsenovski
Architect
Chile Chile
Danijel Arsenovski is senior developer and consultant from Santiago, Chile. His interests include advanced OO programming techniques and refactoring. He holds Microsoft's Solution Developer Certification and is often speaker at Microsoft's technical conferences. Recently, he has been recognized as Microsoft MVP.
He is the author of book "Professional Refactoring in Visual Basic" and "Professional Refactoring in C# and ASP .NET" from Wrox.
From time to time he blogs at http://blog.refactoringin.net

Comments and Discussions

 
GeneralGood article Pinmemberwebooth12-Dec-05 5:17 
GeneralByRef PinmemberJacobOReilly23-Nov-05 2:16 
GeneralJust a quick note PinsussAnonymous20-Jul-05 5:38 
GeneralRefactoring - The bible for programmers who have done naughty things... Pinmemberzitniet22-May-05 0:25 
GeneralExcellent work!!! PinmemberRajesh Pillai17-Dec-04 6:02 
Liked your article very much !!!
Keep up the good work.

 
Enjoy Coding,
Rajesh Pillai

GeneralOh man... this is really great. Pinmemberdan@firewalkersolutions.com14-Dec-04 18:51 
GeneralRe: Oh man... this is really great. PinmemberDanijel Arsenovski15-Dec-04 1:13 
GeneralExcellent! Pinmembermikemdci5-Dec-04 5:10 
GeneralRe: Excellent! PinmemberDanijel Arsenovski6-Dec-04 1:53 
GeneralRe: Excellent! Pinmembermikemdci6-Dec-04 2:13 
GeneralGood Job Paisano! PinmemberGiancarlo Aguilera3-Dec-04 4:18 
GeneralRe: Good Job Paisano! PinmemberDanijel Arsenovski6-Dec-04 1:51 
GeneralRe: Good Job Paisano! Pinmemberjugomkd210-Dec-04 0:45 
GeneralRe: Good Job Paisano! PinmemberWillemM13-Dec-04 22:24 
GeneralRe: Good Job Paisano! PinsussRicardo Stuven20-Jun-05 5:22 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

| Advertise | Privacy | Mobile
Web02 | 2.8.140814.1 | Last Updated 3 Dec 2004
Article Copyright 2004 by Danijel Arsenovski
Everything else Copyright © CodeProject, 1999-2014
Terms of Service
Layout: fixed | fluid