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
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
Private Sub Releasing(Optional ByVal Resource As _
Integer = IS_MISSING_OPTIONAL_PARAM)
If IsMissing(Resource) Then
Else
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
End Class
After:
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
Public Class CAccount
Inherits ECCClass
Private mlngAccountID As Integer
Public Property AccountID() As Integer
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
Private mPersistence As AccountPersistence
Public Sub New()
mcol = New Collection()
mPersistence = New AccountPersistence(Me)
End Sub
Public Sub Clear()
Public ReadOnly Property Count() As Integer
Public ReadOnly Property Item(ByVal Index As Integer) As CAccount
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
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
Public Function Update(Optional ByVal Index As _
Integer = IS_MISSING_INT) As Boolean
Update = mPersistence.Update(Index)
End Function
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
Public Class AccountPersistence
Private mcol As ColAccount
Private mvarSecurityToken As String
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
Delete = True
End If
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
ReDim vParameters(PARMUBOUND, 1)
With oCAccount
.ClassStorage = True
vParameters(PARMNAME, 0) = _
PARMNAMESP_ACCOUNTACCOUNTID
.ClassStorage = False
End With
If Not oDALEng.Execute(SecurityToken, _
SP_D_ACCOUNT, vParameters) Then
Err.Raise
End If
End If
oCAccount = Nothing
mcol.Remove((inx))
Next inx
Delete = True
Erase vParameters
End Function
After:
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
Public Function Delete() As Boolean
Dim LowerLimit As Integer
Dim UpperLimit As Integer
LowerLimit = 1
UpperLimit = mcol.Count
Delete = Delete(LowerLimit, UpperLimit)
End Function
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
Delete = True
End If
If Not SafeCreateObject(oDALEng, OBP_DA_CONNECTION, ErrorNum) Then
Err.Raise
End If
For inx = UpperLimit To LowerLimit Step -1
Next inx
Delete = True
End Function
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()
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
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
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
End Class
After:
Public Class ColAccount
Inherits CollectionBase
Private mPersistence As AccountPersistence
Public Sub New()
mPersistence = New AccountPersistence(Me)
End Sub
Public ReadOnly Property Item(ByVal Index As Integer) As CAccount
Get
If InnerList.Count() = 0 Then
Exit Property
End If
Item = InnerList.Item(Index - 1)
End Get
End Property
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
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
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
For inx = UpperLimit To LowerLimit Step -1
oCAccount = mcol.Item(inx)
If Not oCAccount.IsNew Then
ReDim vParameters(PARMUBOUND, 1)
With oCAccount
.ClassStorage = True
vParameters(PARMNAME, 0) = _
PARMNAMESP_ACCOUNTACCOUNTID
.ClassStorage = False
End With
If Not oDALEng.Execute(SecurityToken, _
SP_D_ACCOUNT, vParameters) Then
Err.Raise
End If
End If
oCAccount = Nothing
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
If mcol.Count = 0 Then
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
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
For inx = LowerLimit To UpperLimit
oEntity = mcol.Item(inx)
If oEntity.Dirty Then
vParameters = PrepareUpdateData(oEntity, vParameters)
If oEntity.IsNew = False Then
SPName = StoredProcUpdate()
Else
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
UpdateCollectionWithReturnedData(rsAccount, oEntity)
End If
End If
End If
Next inx
Update = True
Erase vParameters
End Function
End Class
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
ReDim vParameters(PARMUBOUND, 1)
With account
.ClassStorage = True
vParameters(PARMNAME, 0) = PARMNAMESP_ACCOUNTACCOUNTID
End With
End Function
Protected Overrides Sub UpdateCollectionWithReturnedData(ByVal _
rsAccount As Recordset, ByRef entity As ECCClass)
Dim account As CAccount
account = entity
With account
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
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
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
col = New ArrayList()
Do Until rsAccount.EOF
col.Add(oCAccount)
rsAccount.MoveNext()
Loop
End If
If Not oColAccount.Load(SecurityToken, col) Then
Err.Raise
End If
GetAccount = oColAccount
End Function
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
If Not IsEmptyArray(vParameters) Then
If Not oDALEng.Execute(SecurityToken, _
StoredProcSelect, vParameters, rs) Then
Err.Raise
End If
Else
If Not oDALEng.Execute(SecurityToken, StoredProcSelect, , rs) Then
Err.Raise
End If
End If
oCol = CreateCollectionInstance()
If (Not rs Is Nothing) Then
col = New ArrayList()
Do Until rs.EOF
col.Add(oClass)
rs.MoveNext()
Loop
End If
If Not oCol.Load(SecurityToken, col) Then
Err.Raise
End If
GetECCClass = oCol
Erase vParameters
End Function
Public Function NewClass(ByVal SecurityToken As String) As ColAccount
Dim oECCCol As ECCCollection
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
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
Public Interface <CODE>ISecured
Property SecurityToken() As String
End Interface
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
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
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