Introduction
This article attempts to help you learn about OOP (Object Oriented Programming) by designing and building an example project. Hopefully, this will demonstrate what OO programming is and why it can be useful.
This article is based on the code that was used as part of an in-house introductory VB.NET training course. This course was aimed at developers who had previously only had VBA experience. I have tried to avoid OOP jargon words like polymorphism, encapsulation etc.
Often, OOP is illustrated by using problems that are very simple. The flaw with this approach is that while OOP is useful in making complex problems simpler, it can also just end up making simple problems more complicated.
OOP is also sometimes illustrated with abstract concepts (E.g., Circle Inherits from Shape, or a Cat class and Dog class both Inherit from an Animal class). I'm sorry but for me, all this does is add to the confusion!
The example I have chosen is of importing a text file. Importing text files is a problem that is very real to most of us VB developers; it's a problem that is both easy to understand in concept and yet is complex enough to showcase the benefits of OOP.
You could even use this example application as a basis for writing your own importer.
Our Example Problem...
These are the made-up requirements for our example project:
- We need to support importing the same data from two different formats of files: a CSV file and a certain fixed-width formatted file. (See code download for example files.)
- A user needs to be able to pick a file to import.
- If there are problems during import, the program should provide detailed feedback for the user telling them which lines in the text file could not be imported and why.
- Our app must allow the user to view the data after the import.
- We also suspect that new file formats will need to be imported in the future.
These next two lines are examples of the same data but from the fixed-width format and the CSV (comma-separated-value) files:
1 Value A1 01/Jan/2006Y <-- example line from fixed-width format file
1,Value A1,01/Jan/2006,True <-- example line from csv format file
Note: In a real world app, the data would most likely be saved to a database. For us to do that would require me making you create a database, and that sounds like hassle for both of us! I've decided to say that this is 'cough... beyond the scope of this article'.
Designing an OOP Solution, Where Do We Start?
In designing an OO (Object Oriented) application, one approach is to start with a list of features and then split those features up into classes that are going to be responsible for implementing those features.
In "good" OO applications, each individual object has as few responsibilities as possible, ideally just one main responsibility. So this is how we will describe our objects; in terms of their responsibilities.
This is not some magic formula that will give you a good design, but of all the ideas I've heard, it's the best one. The rest comes, like anything, with experience.
Describing the Solution
Let's have a look at what our project looks like then, and see what we can learn from it about OOP.
The following are the classes that have been written as part of our project and their responsibilities:
- The
ImportForm
class is responsible for interacting with the user. (See screenshot below.)
- The
DemoDataSet
is a DataSet
(another class) that is responsible for holding our data. (Remember, we're allowing the user to view the data after the import.)
- The
ValidationIssue
class is a very simple class that is just responsible for holding the data for an individual issue that is encountered when importing data.
- The
ValidationIssuesList
class is responsible for maintaining a list of ValidationIssue
objects. This will be used to store all the problems our application encounters as it progresses through the import.
This is what our form looks like "in action", just after importing one of the fixed-width format files:
This is the simple 'Made-up' dataset in the DataSet designer in Visual Studio. This is what we'll be importing the data into.
That's the simple stuff out of the way, now let's get on to actually processing the data from the text files...
We could have decided to write two different classes, each one being responsible for importing each of the two different file types. Alternatively, we might think that there is a lot in common in importing files, so we should write one big class that's responsible for importing all of the file types. In fact, with OOP, we can do both of these things at the same time, sharing the responsibilities out between three classes:
- In our project, we have a class called
TextFileImporter
that is responsible for the generic file importing features, the bits that are common to all file imports (opening the file, reading through the file one line at a time, keeping track of the current line number, handling errors etc.).
- The two classes
CSVFileImporter
, XYZFixedWidthFileImporter
will be responsible for dealing with the specific bits for each of our CSV and fixed-width files, respectively.
To achieve this splitting of responsibility, we will make use of Inheritance. This gives us a very easy way of making the XYZFixedWidthFileImporter
class and the CSVFileImporter
class Inherit all the basic importing features from the generic TextFileImporter
class. Then, we just need to Override the bits of the Import process that need to change for each specific class.
As you can see from the class diagram below, the TextFileImporter
class has two subroutines, ImportFile
and ImportLine
. Together, these routines control the overall process of importing. ImportFile
calls ImportLine
in a loop as it reads through the file. Inside the ImportLine
routine, ParseValuesFromLine
is called.
This is a class diagram showing some of the main objects/classes in our project.
ImportFile
and ImportLine
are common to all formats of files that we need to import. The function ParseValuesFromLine
, however, is not. This function must separate each value from a given string, and return an array of strings for the separate fields.
Our TextFileImporter
is not responsible for importing all the specific formats of files, so in this case, it abdicates responsibility, using the MustOverride
keyword, effectively throwing up its hands and saying "someone else is responsible for doing this, it's not my job".
Class TextFileImporter
Public Function ImportFile() As ValidationIssuesList
Using fileReader As New IO.StreamReader(m_filePath)
Try
Do While fileReader.Peek() >= 0
CurrentLineNumber += 1
Dim strLineText As String = fileReader.ReadLine
ImportLine(strLineText)
Loop
Catch ex As Exception
RecordImportIssueForCurrentLine(New _
ValidationIssue(String.Format("Error occurred" & _
" during import of file: {0}", ex.Message)))
End Try
End Using
Return m_importIssues
End Function
Protected Sub ImportLine(ByVal LineText As String)
Dim row As DataRow = m_data.NewRow
Try
Dim astrValues As String()
astrValues = ParseValuesFromLine(LineText)
PutValuesInRow(row, astrValues)
ValidateDataRow(row)
m_data.Rows.Add(row)
Catch ex As Exception
RecordImportIssueForCurrentLine(New _
ValidationIssue("Error during import: " & ex.Message))
End Try
End Sub
Protected MustOverride Function ParseValuesFromLine(ByVal _
LineText As String) As String()
End Class
The CSVFileImporter
is one of the classes which is responsible for dealing with this, so it has a ParseValuesFromLine
function that does just that, parse the string and return an array of separated strings, the values out of an individual line of text.
Class CSVFileImporter
Inherits TextFileImporter
Protected Overrides Function ParseValuesFromLine(ByVal _
LineText As String) As String()
Dim values As String()
values = LineText.Split(","c)
Return values
End Function
End Class
Also, the XYZFixedWidthFileImporter
is another class that is responsible for dealing with this same feature but for a different file format.
Class CSVFileImporter
Inherits TextFileImporter
Protected Overrides Function ParseValuesFromLine(ByVal _
LineText As String) As String()
Dim astrValues(0 To 3) As String
If Not Len(LineText) = 37 Then
Throw New ApplicationException("Line is not" & _
" correct length. Was '" & Len(LineText) & _
"' expected '37'")
End If
astrValues(0) = LineText.Substring(0, 5)
astrValues(1) = LineText.Substring(5, 20)
astrValues(2) = LineText.Substring(25, 11)
astrValues(3) = LineText.Substring(36, 1)
Return astrValues
End Function
End Class
Because we have created different classes to deal each of the different file formats, we have created another responsibility, which is to decide which class to use to import the file that the user has selected. Of course, we could put this code in the form, but then again, the form already has a lot of responsibilities to deal with. Instead, we write another class whose sole responsibility is to create the appropriate object to import files with. This class is called the TextFileImporterFactory
:
Public Class TextFileImporterFactory
Public Function CreateTextFileImporter(ByVal FilePath As String, _
ByVal data As DataTable) As TextFileImporter
Dim importer As TextFileImporter
If Len(FilePath) = 0 Then
Throw New ApplicationException("No File selected")
ElseIf FilePath.ToLower.EndsWith("csv") Then
importer = New CSVFileImporter(data, FilePath)
ElseIf FilePath.ToLower.EndsWith("xyz") Then
importer = New XYZFixedWidthFileImporter(data, FilePath)
Else
Throw New ApplicationException("File type not supported")
End If
Return importer
End Function
End Class
Advantages of OOP
If we were to have designed this project without using the inheritance feature of OOP, then we would have had to have passed around variables describing what type of data we were importing. Also, many of our routines would have contained lots of If
statements for coping with different file formats (If format ="csv" then do this, if format="xyz" then do something else).
Another advantage of this approach is that should we need to support another file format, we can most likely do it without changing any of our existing importing code! This is surely a good thing because if we don't change the code, we don't risk breaking it. The only thing we do need to do is add another class to support the specifics of the new format and change the TextFileImporterFactory
class so that it creates that new class when appropriate.
Further Information (Design Patterns)
Common 'Design patterns' used in this project:
The TextFileImporterFactory
uses the 'Factory' pattern.
The 'Strategy' pattern is also in use here; the ImportForm
is the 'Context', the TextFileImporter
is the 'Strategy', and the XYZFixedWidthFileImporter
and the CSVFileImporter
classes are the 'ConcreteStrategies'.
When you begin to realise what these patterns achieve for you, it helps you design new OO applications. For example, the 'Strategy' pattern can help your program cope with any set of complex algorithms that have behaviours in common, not just importing algorithms.
The idea for this example based approach to OOP comes from a book called 'Design Patterns Explained' by Alan Shalloway and James R Trott.
Exercises For You...
This project is not done... As a learning exercise, why don't you:
- Improve the error handling. E.g., give a better error message than 'Index was outside the bounds of the array' when importing the test data with the errors.csv file.
- If you want to learn about how .NET deals with inheritance and running the different bits of code from classes that inherit from each other, I recommend that you download the source code and step through it in the debugger. Remember to keep an eye on which class you're in as you step through.
- Write a new
XYZFixedWidthFileImporterV2
class to import the second fixed-width format text file (Test Data v2.xyz). Also modify the TextFileImporterFactory
class so that it is able to create the XYZFixedWidthFileImporterV2
at the appropriate time. (Hint: The length of the Line of Text is different between for the two files, you may have to read the first line of the file to determine which object to create.)