5,692,513 members and growing! (16,550 online)
Email Password   helpLost your password?
Languages » VB.NET » General     Advanced License: The Code Project Open License (CPOL)

Refactoring - elixir of youth for legacy VB code

By Danijel Arsenovski

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.
VBWindows, .NET, .NET 1.0, .NET 1.1, NT4, Win2K, WinXP, Win2003, VistaVS.NET2002, Visual Studio, Dev

Posted: 3 Dec 2004
Updated: 3 Dec 2004
Views: 89,752
Bookmarked: 56 times
Announcements
Loading...



Search    
Advanced Search
Sitemap
42 votes for this Article.
Popularity: 7.86 Rating: 4.84 out of 5
0 votes, 0.0%
1
0 votes, 0.0%
2
0 votes, 0.0%
3
3 votes, 7.1%
4
39 votes, 92.9%
5

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, sinc