You can also use the same mechanisms to try to minimize the quantity of code from business domain, but when dealing with domain code much more customization is likely to occur. For example, take a look at this code:
If client.VIP Then order.Discount = ClientDiscounts.VIP End IfIt is clear that these lines of code embed a relevant and important business rule. If you were talking to your client about it, you could most probably explain without difficulty what the code is doing: “If the client is VIP, then apply VIP discount to his order” is pretty much obvious even for a non-programmer reading the code.
On the other hand consider the following few statements:
Dim connection New SqlConnection( _ "Data Source=SHOPSERVER;" + _ "Initial Catalog=ONLINESHOP)" Dim command = New SqlCommand strSql = "Update Client Set Email = " _ + "@Email Where ClientId = @Id" command.Parameters.AddWithValue( _ "@Email", TxtEmail.Text) command.Parameters.AddWithValue( _ "@Id", TxtId.Text) connection.Open() command.Connection = connection command.CommandText = strSql command.ExecuteNonQuery() connection.Close()This code is updating client data in the database, persisting new client email inside Client table from ONLINESHOP database. If you tried to explain this code to your client using the same words, you would probably get some questioning glances. Or, if your client is of more inquisitive sort, he just might rightfully ask you: “But why do you have to persist data?” A perfectly valid question if you think about it. The reason why you persist data is because computer RAM memory is volatile, so the application would lose data each time the computer is shut down. By persisting data to database, you are making sure that data is written to a disc drive. This device is capable of surviving power outage so no data is lost. It is a technological problem that we are resolving with this code. Your client is concerned not to lose data, but how exactly this problem is solved is rarely of much interest to him.
This leads us to the following conclusion: choosing a platform that solves more typical technological problems will make you more productive and will simplify the development and maintenance process. With a more complete platform, both you and your client win.
If you programmed Web Services using the Microsoft SOAP toolkit in VB6 and then programmed web services in .NET, you will know exactly what I am driving at. With the SOAP toolkit there is much more “plumbing” involved than with VB.NET.
A similar shift is now happening with Microsoft .NET framework 3.5 and Visual Studio 2008. Before I explain the impact that new technologies such as LINQ have on VB6 code migration, let’s see how the VB6 migration to .NET is performed in a pre-.NET 3.5 environment.
Upgrading your VB6 code to pre-3.5 .NET employing refactoringIn a previous VSJ article, Refactoring – the elixir of youth for legacy VB code I provided a number of recipes for transforming VB6 code to Visual Basic .NET and pointed out that VB6 to VB.NET transformation has two essential stages: migration and upgrade. During migration, only minimal changes to code are performed in order to make the code execute in exactly the same manner in the VB.NET environment. This stage is to some extent (far from completely) performed with the help of VB Migration Wizard tool that comes bundled with the Visual Studio IDE. In practice, this stage unfortunately often turns out to be the only stage in transformation process. During upgrade, the next stage in the transformation process, freshly migrated code is restructured so that it can take advantage of all the benefits of VB.NET as a strongly statically typed and fully object-oriented language, compared to one generation older object-based VB6. Since migrated VB6 code can have a strong resemblance to poorly structured VB.NET code, I have demonstrated how refactoring can be used to inject new life into this code by improving its design and restructuring it along object oriented design principles and harvesting features available in VB.NET only.
A new style of application developmentSome of the new features in VB 9 are XML literals, local variable type inference, extension methods, lambda expressions and of course LINQ. With .NET framework 3.5 you get a number of implementations that are making use of these new language features. For example, LINQ to XML is a new XML programming interface that will let you query XML document using LINQ syntax; LINQ to SQL will let you access relational database in the same way.
New language features and frameworks emerging in the wake of .NET 3.5 provide an excellent mix for new application design paradigm. Let’s take a look at these acronyms and see how they can help you design applications in efficient and robust manner.
POCO (Plain Old CLR Object) is used to refer to classes that are not forced to inherit other classes or implement interfaces just to make use of environment in which they reside. Not so classes you will use to program your COM+ components. To run your component inside the COM+ environment, your class will have to inherit the System.EnterpriseServices.ServicedComponent. For example, take a look at this mock COM+ component class:
Imports System.EnterpriseServices Public Class MyCOMPlusComponent Inherits ServicedComponent '... End ClassThe problem with such a class is that it is seriously limiting our flexibility:
- Your class can now run only in serviced environment
- Your class cannot inherit any other class; thus reuse potential is severely reduced
Object Relational Mapping (ORM) does persistence for you. In theory, you map your class to table(s) in the database and you let the ORM do its magic. You do not write SQL or stored procedures. All this work is performed by the ORM. Not only have you greatly reduced the amount of code you have to write and maintain, but you generally get advanced features like caching, lazy loading and transactions out of the box. ORM will generally let you switch from one database vendor to another with minimum hassle and even generate a database schema for you. When you write your own SQL, it is notoriously difficult to keep it database neutral.
Dependency Injection (DI) can help minimize dependencies in your application – adhere to the “Program to Interface not Implementation” principle and this will facilitate unit testing your application. When using DI, you program a set of services and then assemble your application with the help of the DI framework. Information on concrete implementation for dependencies is kept in configuration files or in your source code in the form of attributes. Switching from one implementation to another can be as easy as changing the configuration file. Many DI frameworks go further than that and offer additional services by way of a lightweight container concept. So you can get many of the services that heavyweights like MTS provide but without making any sacrifices in your design.
These concepts fit well together in creating a new paradigm for robust and flexible application design that promotes rapid development and rich domain models. If you are just starting to work on a new application, especially if you are using latest version of Visual Studio and .NET framework, you have probably considered at least some of approaches I have mentioned. If not, you will be well advised to do so.
But how does this all fit with legacy application upgrade? After all, a few years back we used to write our own persistence code, MTS was promoted as a panacea and there was no inheritance in VB6. In order to explore this issue, I will examine one such legacy application and will try to upgrade it to .NET under this new design formula.
Engine-Collection-Class design patternOne example of a more elaborate design in VB6 is the Engine-Collection-Class (ECC) pattern. A sample application illustrating this pattern is available for download from MSDN. To download it, type “DesiPat.exe” in the MSDN search box, since URL is pretty much unrepeatable. This pattern provides a structured and uniform approach to application construction in VB6. Its aim is to provide guidance and best practices and to streamline development of enterprise applications in VB6. It also provides an object-oriented approach to application design in VB6, despite VB6 (or COM for that matter) limitations in this department.
The basic premise of the pattern can be described with following statement: Each domain class from your conceptual model is implemented as Class, Engine and Collection classes in your implementation model. These three classes are closely linked so I will refer to them as the “ECC triad”. You can appreciate relationships between them in Figure 1.
Figure 1: Engine-Collection-Class static structure diagram depicting the relationship between three classes in the pattern
As you can see, Engine depends upon both the Class and the Collection, while Collection can hold many instances of Class. Let’s take a closer look at the role each of these classes is fulfilling in this model.
- Engine serves as a sort of faade, an entry point that all clients will use in order to get hold of Collection instances. Both Collection and Class are marked as “PublicNotCreatable” so Engine is the only class clients can instantiate. It also implements a search and retrieval methods that will search Class instances in the database and use search result data to fill the Collection that is then returned to the client. In addition, it is capable of creating a new empty Collection that can be used to create new instances of Class and to persist them to the database later on.
- Collection is a type-safe wrapper over the original VB6 Collection class. For example, Item property of Collection will return a specific Class instance, not just any object. Collection also implements methods that will let you update or delete Class instances and then persist these changes to database. The method used to commit changes to the database is called “Update”.
- Class is used to embed business or domain rules. So one Class can have another Class or Collection of another Class as a property, when there is some kind of association between Classes. Class also has a number of persistence-related properties like: Dirty, IsNew and DeleteFlag that signal correct operations to Collection when committing changes to the database. A Class is generally mapped to one or more tables in the database, either directly or by calling appropriate stored procedures or views.
Upgrading an Engine-Collection-Class application to VB 2008Since many ECC sample applications are concerned with database persistence, I will make use of LINQ to SQL technology to implement persistence in the application. I will also make my domain classes adhere to the POCO principle as much as possible. Finally, I will try to make use of VB.NET language features like structured error handling, inheritance and generics to name a few. With this in mind, let’s take a look at an EEC sample.
Each application constructed along ECC design pattern will consist of number of ECC triads. For illustration purposes, I will upgrade only one such ECC triad to VB 2008. Since uniformity is a strong characteristic of the pattern, a similar approach to upgrade can be applied to any such triad. If you have downloaded the example from MSDN site, you can take a look at IOBPUserInfo project used to maintain user information. The project is a component that consists of three classes:
- IOBPUserInfoEng – represents Engine in ECC
- ColUserInfo – represents Collection in ECC
- CUserInfo – represents Class in ECC
Upgrading ClassSince business-related code is contained inside Class, let’s start by upgrading CUserInfo. This code has greatest value for the client and therefore it is important to preserve it without changing any of the business rules from the original application. First take a look at CUserInfo code. I will omit a lot of less relevant code here, but if you are interested in taking a look at complete application, and in giving it a test ride, download the sample application from the MSDN site. So let’s take a look at the code:
'Set to true when class changes Private mblnDirty As Boolean 'Set to true when new class first initialized. Private mblnIsNew As Boolean Private mblnDeleteFlag As Variant Private mlngUserNumber As Variant Private mvarUserID As Variant Private mvarPassword As Variant Private mvarName As Variant Private mvarAddress As Variant Private mblnDisabledFlag As Variant Private mvarRecordTimestamp As Variant Private mcolAccount As Object 'etc Public Property Let EmailAddress(ByVal vData As Variant) '... mvarEmailAddress = LetUIValue(vData, vbVariant) Me.Dirty = True '... End Property Public Property Get Account() As Object On Error GoTo ErrorHandler Dim engAccount As Object Dim ErrorNum As Long If mcolAccount Is Nothing Then If Not SafeCreateObject(engAccount, _ OBP_ACCOUNT_ENG, ErrorNum) Then Err.Raise ERR_ACCOUNTSAFECREATEFAILED, _ "IOBPUserInfoEng.Account Property", _ "Unable to create "& OBP_ACCOUNT_ENG & _ ". Return Code was: " & ErrorNum GoTo CleanExit End If 'Only get the non-deleted records. Set mcolAccount = engAccount.Search( _ SecurityToken:=SecurityToken, UserNumber:=Me.UserNumber) End If Set Account = mcolAccount CleanExit: 'See error handling code 'in Property Get Dirty() routine End Property Public Property Get Dirty() As Variant On Error GoTo ErrorHandler Dirty = mblnDirty CleanExit: Exit Property ErrorHandler: Dim lErrLine As Long Dim lErrNumber As Long Dim strErrSource As String Dim strErrDescription As String 'Preserve Error Information lErrNumber = Err.Number lErrLine = Erl strErrSource = Err.Source strErrDescription = Err.Description Call HandleException(SecurityToken, _ lErrNumber, strErrDescription, lErrLine, strErrSource) Exit Property End Property Friend Property Let Dirty(ByVal vData As Variant) On Error GoTo ErrorHandler If VarType(vData) <> vbBoolean And IsEmpty(vData) Then 'raise error Err.Raise ERR_USERINFOINVALIDDIRTY, _ "CUserInfo.Dirty LET", _ "Invalid Data Type Expected Boolean." GoTo CleanExit End If 'Set Value mblnDirty = vData CleanExit: 'See error handling code in Property Get Dirty() routine End PropertyBasically, CUserInfo class has a number of properties. These are used to record important personal data like user name, password, name, address, phone, city, postal code, email, time of last login etc. There is also Account property that will let you obtain all Account objects that belong to a certain user. We could say that these properties make up a core of business-related logic expressed in a UserInfo triad and consequently this is the code that has the biggest value from business point of view.
Besides these business logic related properties, CUserInfo has some properties relevant for persistence logic. A number of Boolean flag properties are used to indicate the state of User object in relation to persistence. IsNew indicates that instance is new and that should be saved to database, Dirty indicates the value of some property has been changed so that update of the instance is necessary. DeleteFlag indicates that an instance should be eliminated from the database.
As you can see, the code is quite verbose and written in “defensive” style, with a lot of emphasis on error handling code. You will also appreciate that all public members expose VB6 Variant or Object type in the signature. This works against static type checking but is necessary in VB6 in order for this component to be used by scripting clients. One such client is ASP page and since ASP technology uses weakly typed VBScript or JScript, UserInfo has to expose Variant type in member signatures.
When upgrading CUserInfo, I will preserve business-related properties, but I will eliminate persistence-related properties. Using LINQ to SQL, all persistence-related control is implicit and performed by the framework itself. That way, our entity classes can preserve “persistence ignorance”. In order to make CUserInfo managed by LINQ to SQL, we need to provide some metadata to the framework. For example, we need to indicate which table in the database the class is mapped to. This can be done by means of some configuration files, or by applying attributes to CUserInfo class are we’ll choose the latter:
<Table(Name:="UserInfo")> _ Public Class UserInfoWe also need to provide information on property to column mappings.
Take a look at the listing below to see CUserInfo code. By the way, the class has been renamed as UserInfo as “C” prefix is not recommended by new .NET framework naming guidelines.
Option Explicit On Option Strict On Imports System.Data.Linq.Mapping <Table(Name:="UserInfo")> Public Class UserInfo Private userNumberValue As Long Private userIdValue As String Private nameValue As String Private violationCountValue As Long Private emailAddressValue As String Private userTypeIdValue As Long 'Private accountsValue As List(Of Account) - (not yet upgraded) 'other private variables... <Column(Name:="UserNumber", DbType:="int", CanBeNull:=False, IsPrimaryKey:=True, _ IsDbGenerated:=False)> Public Property UserNumber() As Long Get Return userNumberValue End Get Set(ByVal value As Long) userNumberValue = value End Set End Property <Column(Name:="Name", DbType:="NVarChar(50) NOT NULL", CanBeNull:=False)> _ Public Property Name() As String Get Return nameValue End Get Set(ByVal value As String) nameValue = value End Set End Property 'Other properties... End Class
Eliminating CollectionCollection is basically a type safe wrapper over the VB6 Collection container. Methods like Clear or property Count delegate execution to internal collection:
Private mCol As Collection ' Used to store the Classes Public Property Get Count() As Variant On Error GoTo ErrorHandler If mCol Is Nothing Then Count = 0 Else Count = mCol.Count End If '... End PropertyColUserInfo Collection class also has a number of persistence-related methods. Take a look at the listing below for Delete method. This method is calling spDUserInfo stored procedure, a simple SQL DELETE statement, again programmed in verbose and defensive manner so it is more than 30 lines long. The rest of stored procedures also implements simple CRUD operations.
Public Function Delete(Optional ByVal Index _ As Variant) As Variant On Error GoTo ErrorHandler Dim ErrorNum As Long Dim oDALEng As IOBPDA.IOBPConnection Dim oCUserInfo As CUserInfo Dim vParameters() As Variant Dim LowerLimit As Long Dim UpperLimit As Long Dim inx As Long Dim SPName As String Delete = False If Me.Count = 0 Then 'Nothing to Update Delete = True GoTo CleanExit End If 'If Index is supplied then Delete only the supplied record If Not IsMissing(Index) Then If Index < 1 Or Index > Me.Count Then Err.Raise ERR_USERINFOINDEXOUTOFRANGE, _ "ColUserInfo.Delete PROC", "Index out of range" GoTo CleanExit Else LowerLimit = Index UpperLimit = Index End If Else LowerLimit = 1 UpperLimit = Me.Count End If If Not SafeCreateObject(oDALEng, _ OBP_DA_CONNECTION, ErrorNum) Then Err.Raise ERR_USERINFOSAFECREATEFAILED, _ "ColUserInfo.Delete PROC", "Unable to create " _ & OBP_DA_CONNECTION & _ ". Return Code was: " & ErrorNum GoTo CleanExit End If For inx = UpperLimit To LowerLimit Step -1 Set oCUserInfo = Me.Item(inx) If Not oCUserInfo.IsNew Then 'Delete from DB If Not New ReDim vParameters(PARMUBOUND, 1) With oCUserInfo .ClassStorage = True 'Fill Parameter Array vParameters(PARMNAME, 0) = _ PARMNAMESP_USERINFOUSERNUMBER vParameters(PARMTYPE, 0) = _ PARMTYPESP_USERINFOUSERNUMBER vParameters(PARMLENGTH, 0) = 4 vParameters(PARMDIR, 0) = adInput vParameters(PARMVALUE, 0) = .UserNumber vParameters(PARMNAME, 1) = _ PARMNAMESP_USERINFORECORDTIMESTAMP vParameters(PARMTYPE, 1) = _ PARMTYPESP_USERINFORECORDTIMESTAMP vParameters(PARMLENGTH, 1) = 8 vParameters(PARMDIR, 1) = adInput vParameters(PARMVALUE, 1) = .RecordTimestamp .ClassStorage = False End With If Not oDALEng.Execute(SecurityToken, SP_D_USERINFO, _ vParameters) Then Err.Raise ERR_USERINFODALDELETEFAILED, _ "ColUserInfo.Delete PROC", _ "Delete Failed. SPName was: " & SP_D_USERINFO GoTo CleanExit End If End If Set oCUserInfo = Nothing 'Remove from Collection mCol.Remove (inx) Next Delete = True CleanExit: Erase vParameters() Set oDALEng = Nothing Set oCUserInfo = Nothing Exit Function ErrorHandler: '... End FunctionWhen using LINQ to SQL, you will generally let the framework generate the SQL “under the bonnet”. Also, in VB.NET since the 2005 version you can use generic type-safe containers provided by the .NET framework.
What does this tell us? You have no need for Collection class or simple CRUD stored procedures in VB 2008! So, neither the Collection class nor related stored procedures will be upgraded. You have just saved yourself from upgrading or maintaining a huge quantity of code.
Upgrading EngineIOBPUserInfoEng contains code that queries the database and populates collection with UserInfo instances. For this purpose it uses parameterized stored procedures that contain SQL query code. Similar to persistence related code in Collection class, this code can be replaced by functionality provided by LINQ to SQL System.Data.Linq.DataContext and System.Data.Linq.Table classes in the .NET framework. We can turn easily turn or Engine class into the thin wrapper over DataContext and Table classes. Take a look at the listing below for upgraded Engine code which uses the more straightforward name “UserEngine” for Engine class.
Option Explicit On Option Strict On Imports System.Data.Linq Public Class UserInfoEngine 'TODO: read connection string from configuration Private Const ConnectionString As String = "Data Source=TESLATEAM;" & _ "Initial Catalog=OnlineBill; Integrated Security=True" Private contextValue As DataContext Private userInfoTable As Table(Of UserInfo) Public Sub New() contextValue = New DataContext(ConnectionString) userInfoTable = contextValue.GetTable( Of UserInfo)() End Sub Public ReadOnly Property Table() As Table(Of UserInfo) Get Return userInfoTable End Get End Property Public Sub SubmitChanges() contextValue.SubmitChanges() End Sub End Class
Model, unit test and implementIn a typical VB6 application, huge quantities of existing code have been made functionally obsolete by new technologies available in Visual Studio 2008. Your legacy code is likely to suffer from number of problems:
- Code proliferation and duplication because of limited object-oriented capabilities in VB6
- Obsolete coding and naming conventions coupled with verbosity of VB6 syntax
- Poor structure, long methods and use of structured programming constructs like global functions in VB6 Module
- Legacy error handling
- Business logic spread between VB and SQL code
All these deficiencies of legacy code coupled with benefits of VB 2008 imply that integral upgrade processes are not practical any more. Instead, you should only reuse those parts of code that still have direct value from a business point of view. This means that as a part of the upgrade process you should go back to the drawing board. The process can be performed in following steps:
- Start by modeling your problem domain in object-oriented terms
- Write unit tests for each functionality your system should implement.
- Implement functionality under test by upgrading specific sections of code
ConclusionThe latest versions of Visual Basic and the .NET framework are changing the VB programming landscape. New features bring new levels of productivity and expressiveness. In this new landscape, integral upgrade of legacy VB6 applications is rapidly losing its purpose. Those still upgrading legacy VB6 applications mechanically are at risk of producing a lot of boilerplate code that should not even be written any more. Such an approach will hinder productivity, make maintenance cumbersome and will stand in the way of the implementation of new features. Instead of an indiscriminate upgrade of your VB6 code, go back to the problem domain, rethink your design and let unit tests guide you while you upgrade only those pieces of code that are relevant from the business point of view.
Danijel Arsenovski is the author of Professional Refactoring in Visual Basic, published by Wrox. Currently, he works as Product and Solutions Architect at Excelsys S.A, designing Internet banking solutions for numerous clients in the region. He started experimenting with refactoring while overhauling a huge banking system, and he hasn’t lost interest in refactoring since. You can find his blog at blog.vbrefactoring.com.