VSM Cover Story
Refactor Your Way to Migration Success
Take advantage of the top ten refactorings for VB6-.NET migration to create easier-to-read and far more robust .NET applications. These refactorings pick up where the Migration Wizard leaves off.
Technology Toolbox: Visual Basic, ASP.NET, XML
Microsoft's .NET initiative is tied closely with the concepts of SOA and Web services. Indeed, .NET was created with the intention of being an excellent environment for the development and use of service-based applications. However, companies often have to overcome a significant number of obstacles while trying to jump on .NET bandwagon, not the least of which is deciding what to do with their legacy code and legacy applications. These applications are often difficult to integrate into the service-oriented world.
For example, there is still a huge amount of legacy VB code in operation. VB6 has proved to be an excellent tool for the rapid fabrication of components and COM-based development, but it lacks agility and productiveness when used for Web services development.
This brings us to a central pair of questions: What are your options if your operation still depends on a legacy code base, and what can you do to move closer to service-oriented architecture without abandoning your legacy code base completely?
You have three options available to you in this circumstance. First, you can stick with VB6 and your legacy VB code base. Second, you can use .NET-COM interoperability to create a "middle way." This means keeping your COM components and using them from your newly developed .NET code. Third, you can move completely to .NET and upgrade your legacy VB code to VB.NET.
There are advantages and drawbacks to each of these choices; each organization needs to analyze all the options available to it, and then choose the path that best suits its requirements. Upgrading a VB6 application to .NET requires a lot of initial work and investment, but I think it is the winning option in the long run for most circumstances. The success of this option largely depends on your capacity to upgrade legacy code. But once the upgrade is performed, you have the latest technology and the most productive tools on the market at your disposal.
The third option—to move completely to .NET—is interesting for another reason: Many companies regard it as the most costly. However, approached correctly and with the right set of tools and techniques, upgrading VB6 code to .NET doesn't have to be a daunting task.
Upgrading VB6 code to .NET does presents a number of challenges, but refactoring techniques can simplify many of them. Refactoring can give you the capacity to perform a full upgrade of your legacy code, and thus reap all the benefits of VB.NET and the .NET platform.
There is a downside: Upgrading to VB.NET is not a simple task, even with help from the Migration Wizard. First, Microsoft broke compatibility between VB6 and VB.NET in the process of preparing VB for the .NET platform. Some features in VB.NET are the direct result of Microsoft's attempt to ease the path for upgrading from legacy VB code to .NET. Even so, you face a number of issues that aren't easily resolved. In the end, you will have to finish manually the job the Migration Wizard started, writing some code yourself, before you can get a fully functional application that replicates the behavior of its legacy counterpart.
Once you migrate your application so it works correctly, you can say that the first half of your work is done. However, such an application requires more work before you can begin enjoying the advantages that the .NET Framework and the current version of VB.NET provide.
Fortunately, refactoring techniques give you an excellent tool for the second part of the task. Applying refactoring enables you to transform your recently migrated code, so it can harvest all the benefits of fully object-oriented, Statically-typed, .NET versions of Visual Basic.
What Is Refactoring?
It might be helpful to take a quick look at what refactoring is, and how it can benefit you, before looking more closely at the ways you can use it to help migrate your code to .NET. According to Martin Fowler, author of well-known books on refactoring, ?Refactoring is the process of changing a software system in such a way that it does not alter the external behaviour of the code, yet improves its internal structure.?
For example, assume you have a Purchase class:
Public Class Purchase
Private customer As Customer
Private items As List(Of PurchaseItem)
Public Function CalculateTotal() As Decimal
Dim total As Decimal = 0
For Each pItem As PurchaseItem In items
total += pItem.Price
Next
If (customer.NumberOfPurchases > 3) Then
total *= 0.97D
End If
Return total
End Function
End Class
Now assume that you want to improve this class. Take the literal 0.97D and the literal 3 from the body of the method, and replace it with constants. Next, place code that calculates a customer discount into a separate method (see Listing 1).
These changes give you several benefits. First, it's much easier to understand the purpose of the second part of original CalculateTotal method. You can see it is used to apply discount for returning customers. It's also more obvious how and when you apply the discount. Also, you can see that it's easier to understand the meaning of literal values in the code. Defining these literals as constants helps you prevent unnecessary code duplication if you use them in other parts of your application. It also makes it easier to edit these values if they change in the future because you need to change them only at the place where you declare them.
This example illustrates two standard refactorings: "Extract method" and "Replace Magic Number with Symbolic Constant." You perform them to simplify one method and to give meaning to some constant values.
Note that you apply refactorings in order to correct some design deficiencies in code. You'll encounter several of these deficiencies if you prepare to migrate an application from VB6 to .NET. After all, there is no implementation inheritance in VB6, no strict type checking, no constructors, no structured-error handling, no namespaces, and quite a few other deficiencies you'll encounter, as well. There is also no way to implement these features automatically, or through some "migration wizardry."
What you need to do at this point is to upgrade legacy code completely. You need to do more than update your code, so it executes correctly; you also need to refactor it, so it conforms to the spirit of VB.NET and uses VB.NET's capacities to the fullest extent possible. You also need to perform a significant overhaul of the code you migrate. Refactoring is your best tool for accomplishing this. To that end, these ten tips are the most important kinds of refactorings you'll have to implement to migrate your VB6 applications to VB.NET successfully. This is a list that covers the top points you'll need to address when performing a migration. It is by no means all-inclusive, and you might have other issues that you would list higher. In any case, following these recommendations will simplify the process of migrating your applications to .NET.
The refactorings described in this article rely on a shopping cart library example. This sample app will gives you a real-world context for considering what might otherwise be abstract concepts. The VB6 version of the app is a mix of ASP code and a VB6 COM component; you use it to implement an online store that sells CDs and DVDs. I'll show you how to upgrade and refactor the COM component code in this article, with a little help from the Migration Wizard. (see Figure 1).
Refactoring 1: Declare types explicitly (activate Option Strict). VB6 (and earlier versions VB) didn't enforce Static-type checking. In fact, Static-type checking remains optional in VB.NET, but it's easily turned on and it enforces rules on your .NET code that will make debugging far easier over the long term. All it takes to activate this feature is to place "Option Strict On" statement at the top of the source-code file. After that, compiler rejects all incomplete declarations not containing type information (in other words, declarations that don't include "As" in the declaration).
As you implement strict type checking, you must use the conversion functions to perform all narrowing and rounding conversions explicitly. Doing so gives you many benefits. For example, it reduces the number of bugs resulting from rounding and narrowing errors; it improves code readability because the programmer can think in terms of known abstractions and types; it gives you performance benefits that result from compile-time type resolution; and it activates Visual Studio's IntelliSense and Dynamic Help. Visual Studio isn't capable of providing these two productive features unless you declare variable types explicitly.
The process of migration deactivates Option Strict in the code transformed. Implementing Static-type checking in such code requires that you infer type for all declared elements where this information isn't already present. You can infer type information from literal value assignments, using type information from other assemblies and basing your information on the type of return value of conversion functions. Finally, narrowing conversions aren't performed implicitly any more, so you must add conversion functions to finish the transformation of code into the Statically-typed form.
Let's take a look how you implement this in the sample library. Module Util contains the function CalculatePoints, which adds all the rating points for a certain item. You calculate the points as the sum of all ratings, so the function receives an array of ratings as a parameter. After you activate Option Strict, the compiler displays this error:
Option Strict On disallows implicit conversions
from Double to Short.
The user-defined type, Rating, includes a property named Description. This property can hold colorful words like "masterpiece," "bad spaghetti," and so on. Another property, Value (see type Rating in Figure 1) that customers can use to rate a given CD or DVD—four and a half stars, for example. This property is of type Double. However, the local variable Sum in the CalculatePoints function is of type Short. The code worked when Option Strict was deactivated, but it also introduced a bug:
Sum = Sum + ratings(I).Value
Each time this line executes, the narrowing conversion from Double to Short is performed. All decimal parts of points are lost, so 4.5D is transformed into a 4. Ok, this isn't as serious as a Patriot Missile bug, but the code doesn't work as intended and degrades the quality of your ratings. You need to not only refactor this code, but also eliminate the bug. The only remedy is to change the type of local variable Sum and type of function return value to Double.
The original code looks like this:
Option Strict Off
Option Explicit On
'...
Public Function CalculatePoints(ByRef ratings() _
As ShoppingBasket.Rating) As Short
Dim I As Integer
Dim Sum As Short
For I = LBound(ratings) To UBound(ratings)
Sum = Sum + ratings(I).Value
Next I
CalculatePoints = Sum
End Function
Your job is to refactor the code and fix the error.
Option Strict On
Option Explicit On
'...
Public Function CalculatePoints(ByRef ratings() _
As ShoppingBasket.Rating) As Double
Dim I As Integer
Dim Sum As Double
For I = LBound(ratings) To UBound(ratings)
Sum = Sum + ratings(I).Value
Next I
CalculatePoints = Sum
End Function
Refactoring 2: Transform procedural design to object-oriented design. The typical case of procedural design occurs when you separate data and behaviour. Implementing object-oriented design requires that your data and behaviour go together in the form of a class or structure.
You can often spot procedural design in module elements. In one sense, a module construct in VB is a remnant from its procedural days. Modules generally contain globally accessible functions. These functions represent behavior, but lack the data needed to form objects.
Another good trace of procedural design is User Defined Types (UDTs). The migration process transforms UDTs into structures. Structure constructs in VB.NET are similar to class constructs, with one exception. Structures cannot be inherited. You can leave such structures as-is if you think you will never have to extend the newly created structure. However, you can transform a structure into a class easily if you think you need to leave room for future changes. All you need to do is to change the declaration.
I've noted that the best way to approach this particular problem is to bring the app's data and behavior together. Migrating the app creates this structure, which replaces the UDT from the VB6 version of the application:
Public Structure Rating
Dim Value As Double
Dim Description As String
End Structure
The Util module also includes this function:
Public Function IsGreater(ByRef compared As _
ShoppingBasket.Rating, ByRef compareTo _
As ShoppingBasket.Rating) As Boolean
If compared.Value > compareTo.Value Then
IsGreater = True
Else
IsGreater = False
End If
End Function
A module function is similar to a shared function, so you should act differently in this case. You can eliminate the first parameter if you move this function and make it instance method, because it now refers to the Value property of structure Rating.
Another module function, CalculatePoints, also performs operation related to ratings. You can move this function as-is to the Rating structure with a single modification: Simply mark it as Shared. Once you do that, you can eliminate the Util module from the project:
Public Structure Rating
Dim Value As Double
Dim Description As String
Public Function IsGreater(ByRef compareTo _
As ShoppingBasket.Rating) As Boolean
If Me.Value > compareTo.Value Then
IsGreater = True
Else
IsGreater = False
End If
End Function
Public Shared Function CalculatePoints( _
ByRef ratings()As _
ShoppingBasket.Rating) As Short
Dim I As Integer
Dim Sum As Double
For I = LBound(ratings) To _
UBound(ratings)
Sum = Sum + ratings(I).Value
Next I
CalculatePoints = CShort(Sum)
End Function
End Structure
Refactoring 3: Extract the parent class. There is no implementation inheritance in VB6, and this can often be a source of significant duplication in your code. Duplicate code is a serious design flaw, and you should attempt to amend this, without exception. In fact, duplicate code is often the first step down the path toward creating a maintenance nightmare. Once you have logic scattered in multiple places in your code, and find you need to modify something, tracing all those places becomes almost impossible. This means that you are likely to introduce bugs because the logic was changed in one place or even several places, but not everywhere. Also, this code is difficult to read and understand which can leave you with a difficult-to-answer question: "I have two methods with same name, and they perform the same general action—which one should I use?" The problems you can introduce with code duplication are many and generally serious, so it behoves you to eliminate this duplication wherever possible.
VB6 included a mechanism that let you declare and implement an interface. You should look out for this code because there's a good chance you'll find some duplication between classes implementing the same interface.
Classes implementing the same interface represent a probable candidate for extraction to a common base class. You should also search for copied properties and methods because this duplication can often be resolved by extracting common base classes, as well.
VB.NET allows you to use implementation inheritance, so the solution is to introduce a new class that contains all common properties and methods. All that remains is to make existing classes inherit newly extracted parent class.
You can find this type of example in the sample application in the IItem interface for the CD and DVD classes, which both contain the code that implements methods defined in the IItem interface. There are no differences in implementation, so you will find this code duplicated in the CD and DVD classes. Begin by extracting a common base abstract class, Item. This way, the Item class contains all the common code that implements properties declared in IItem interface.
The VB6 version of the code looks like this for the CD Class:
Public Class CD
Implements IItem
Private mItemId As String
'...
Public Property IItem_ItemId() As String _
Implements IItem.ItemId
Get
IItem_ItemId = mItemId
End Get
Set(ByVal Value As String)
mItemId = Value
End Set
End Property
'...
The code for implementing the DVD class in VB6 is identical:
Public Class DVD
Implements IItem
Private mItemId As String
'...
Public Property IItem_ItemId() As String _
Implements IItem.ItemId
Get
IItem_ItemId = mItemId
End Get
Set(ByVal Value As String)
mItemId = Value
End Set
End Property
'...
The refactored solution is fairly straightforward. You begin by creating a new Item class:
Public MustInherit Class Item
Implements IItem
Private mItemId As String
'...
Public Property IItem_ItemId() As String _
Implements IItem.ItemId
Get
IItem_ItemId = mItemId
End Get
Set(ByVal Value As String)
mItemId = Value
End Set
End Property
'rest of IItem members
Once you create the Item class, it is a simple matter to create your new CD class:
Public Class CD
Inherits Item
'...
The DVD class is equally simple to create:
Public Class DVD
Inherits Item
'...
It might also be useful to take a closer look at the new class of the application after you extract the Item class (see Figure 2).
Refactoring 4: Introduce a constructor. You can use VB.NET's Constructor method to replace VB6's Class_Initialize method. Constructor methods can be parameterized, so you can put the constructor to a new use, related to an object state that wasn't previously available with Class_Initialize method.
Object state must always be consistent in object-oriented programming. Object state is the set of all properties and fields available for a given object. At no point in time should you leave any of the object's properties or fields undefined. Once you define the parameterized constructor for a class, you can create an instance for a class only after you provide all the constructor's parameters because the default constructor is no longer available. You use constructor's parameter data to initialize class properties, which enables you to control the creation of the object and permits initialization only if all the necessary data is supplied to constructor method.
The sample app's Customer class does a good job of illustrating how to introduce a constructor method. Note that the Migration Wizard renames field mId to mId_Renamed:
Public Class Customer
Public Sub New(ByVal id As String, _
ByVal firstName As String, ByVal lastName _
As String)
mId_Renamed = id
mFirstName = firstName
mLastName = lastName
End Sub
Refactoring 5: Replace untyped collections with generics. Generics are a new feature in Visual Basic 2005. Generic types let you declare the specific type acted upon, thus providing a type-safe environment that eliminates need for casting or testing the type of object.
The VB6 sample application uses a Collection class inside the ShoppingBasket class to hold items that customer has selected. After Option Strict is activated, you need to add code to cast items from the collection into IItem type. After the migration, the Wizard uses the Collection class from the Microsoft.VisualBasic namespace in place of the VB6 Collection. If you use some generic container class instead, you can avoid the casting, and you can make your container type safe. This means that only instances implementing IItem interface are allowed into the container.
You can use the type safe List container class from System.Collections.Generic namespace to replace Microsoft.VisualBasic.Collection, because it is also a container that allows individual items to be accessed using the item index (see Listing 2).
Refactoring 6: Organize classes with Namespaces. In VB6, you could reference programming elements from code by their ProgID (Programmatic IDentifier). A ProgID consists of the project name and class name, and it lets you reference individual classes in the form of ProjectName.ClassName. You use namespaces in a similar way in .NET to organize classes. The names aren't limited to only two levels in .NET; they can now form hierarchical structures and span multiple files.
You place the ShoppingBasket and Customer classes into the namespace MyCompany.ShoppingBasket; you place the Item, IItem, CD, and DVD classes into the MyCompany.ShoppingBasket.Item namespace:
Namespace MyCompany.ShoppingBasket
Public Class ShoppingBasket
'...
End Class
End Namespace
'etc.
Refactoring 7: Replace legacy with structured error handling. In VB6, you generally implemented error handling with a combination of a label and the Goto keyword, or by using the ?On Error Resume Next? statement. The Goto keyword is an example of legacy, unstructured programming, and it can contribute heavily to complicated spaghetti code, with awkward jumps in execution flow that are difficult to understand and follow. Similar problems are inherent to the "Resume Next" construct, where error-handling code is scattered throughout the method and can be left unimplemented quite easily.
In VB.NET, you can use structured Try...Catch....Finally blocks. Also, the exceptions are now fully-fledged types on par with any other class that can be instantiated and extended.
The original code in from the VB6 application is relatively straightforward:
Public Sub CheckOut()
Dim Connection As ADODB.Connection
Dim Command_Renamed As ADODB.Command
Dim Item As IItem
On Error GoTo ErrorHandler
Connection = New ADODB.Connection
'...
Exit Sub
ErrorHandler:
Err.Raise(vbObjectError + 101, _
"ShoppingBasket", "Database error")
Refactoring the code gives you something like this:
'our new custom exception class
Public Class DBException
Inherits System.ApplicationException
Sub New(ByVal source As String, ByVal msg As String)
MyBase.New(msg)
Me.Source = source
End Sub
End Class
Public Sub CheckOut()
Dim Connection As ADODB.Connection
Dim Command_Renamed As ADODB.Command
Dim Item As IItem
Try
Connection = New ADODB.Connection
'...
Catch Ex As Exception
Throw New DBException( _
"ShoppingBasket", "Database error")
End Try
Refactoring 8: Transform plain comments into XML comments. One neat new feature in Visual Studio 2005 is the XML comments that are integrated with IntelliSense. As you type the code in Visual Studio, the IDE shows XML comments displayed in a yellow tool-tip window, along with usual IntelliSense Help.
This feature can save you a lot of time that would otherwise be spent browsing the documentation. It also makes you treat the documentation as an integral part of the project, rather than as some second-rate task left until another day when you aren't as busy.
If your code already has some plain comments, it is a good idea to transform them into XML format (see Figure 3).
Refactoring 9: Replace setting objects to "Nothing" with a "Using" block. One of the most significant new features in the .NET Framework is a tracing garbage collector. In COM, the garbage collector was based on reference counting, so you often had to set objects to Nothing to decrement the reference count and release memory in an orderly manner.
You cannot release memory in this manner in VB.NET because of the non-deterministic behavior of the .NET garbage collector. Note that you rarely need to worry about the garbage collection because .NET runtime takes care of memory management. Under certain circumstances, however, you still need to be able to control resource disposal in an explicit manner. Limited resources should be released as fast as possible. In this circumstance, you use the "Using" statement. Resources are disposed immediately after the Using block executes, even in the case of exceptions. The only condition for an object provided to a Using statement is that it must implement the IDisposable interface.
This COM-based code sets the connection to Nothing:
Dim Connection As ADODB.Connection
Connection = New ADODB.Connection
Connection.ConnectionString = _
"Provider=Microsoft.Jet.OLEDB.4.0;" & _
"Data Source=D:\projects\OnlineShop\" & _
"db.mdb;Persist Security Info=False"
Connection.Open()
'...
Connection.Close()
Connection = Nothing
Updating the code with the Using statement takes only a handful of lines of code:
'Also switched from ADO to ADO .Net
Using Connection As System.Data.IDbConnection = _
New OleDbConnection( _
"Provider=Microsoft.Jet.OLEDB.4.0;" & _
"Data Source=D:\projects\OnlineShop\" & _
"db.mdb;Persist Security Info=False")
Connection.Open()
'...
Connection.Close()
Connection = Nothing
End Using
Refactoring 10: Replace optional parameters with overloaded methods. In VB.NET, VB6's IsMissing function has been made obsolete, and all optional parameters must have the default value. During migration, the Migration Wizard attempts to resolve these issues. And herein lays the first problem with optional parameters.
Before you refactor the method with optional parameters, you need to take care of an upgrade quirk produced by the Migration Wizard. The Wizard assigns an arbitrary default value to primitive type parameters. For example, Short is given the default value of zero.
The original VB6 declaration looks like this:
Public Sub SaveTemporarily(Optional numberOfDays _
As Short)
The Migration Wizard transforms this code into something like this:
Public Sub SaveTemporarily(Optional ByRef _
numberOfDays As Short = 0)
In code, IsMissing function is replaced with IsNothing:
' UPGRADE_NOTE: IsMissing() was changed to
' IsNothing().
If IsNothing(numberOfDays) Then
numberOfDays = 7
End If
You need to set the parameter default value to Nothing to make the code execute correctly. VB.NET's autoboxing feature means you can treat primitive types as regular objects:
Public Sub SaveTemporarily(Optional ByRef _
numberOfDays As Short = Nothing)
If IsNothing(numberOfDays) Then
numberOfDays = 7
End If
'...
If the method is called without passing the parameter numberOfDays, the IsNothing function evaluates correctly. This means replacing the IsMissing function with the IsNothing function now works as expected. It also means you don't need to change the way the code behaves; the problem the Migration Wizard introduced is solved.
Optional parameters in VB.NET always have a default value, so you can use two overloaded methods instead. This shortens the original method because it eliminates the conditional code that inspects parameter value:
'Before:
Public Sub SaveTemporarily(Optional ByRef _
numberOfDays As Short = Nothing)
If IsNothing(numberOfDays) Then
numberOfDays = 7
End If
'...
End Sub
'After:
Public Overloads Sub SaveTemporarily(ByRef _
numberOfDays As Short)
'...
End Sub
'Replace literal with constant
Private Const DEFAULT_NUMBER_OF_DAYS _
As Short = 7
Public Overloads Sub SaveTemporarily()
SaveTemporarily(DEFAULT_NUMBER_OF_DAYS)
End Sub
The benefit of the last transformation isn't that obvious in this case because the method is short and has only one optional parameter. However, you can improve the code's clarity significantly if you replace method with optional parameters with overloaded methods when there is more then one optional parameter in play and the body of method is longer.
That's it for the specific listing of refactorings. One of my goals throughout this article has been to illustrate how eminently doable most of these refactorings are. It can seem a daunting task at the outset, but once you set your mind to it, it's a simple matter of following through a set number of steps you must execute. A secondary goal has been to illustrate that the process of upgrading your VB6 code to .NET isn't as complicated as it is generally perceived.
If you think refactoring might work for you as you upgrade legacy Visual Basic code, you can check out some Visual Basic refactoring tools. One free, but basic, tool is available for download from Microsoft's site: http://msdn.microsoft.com/vbasic/downloads/tools/refactor/. There are also other, more complete commercial tools.
And, of course, refactoring is useful even if you're not upgrading code. It is a great way to maintain the design and quality of your code for all circumstances, not just when migrating your code.