Adding Methods to Classes
Methods are functions and subroutines that just happen to belong to a class or structure. We covered structures in Chapter 5, so all of our examples will be demonstrated using classes in this chapter.
Anything you can do with a procedure in a module, you can do with a procedure in a class. The only rule when it comes to methods is subjective measure of good taste. Personally, I like Beethoven and the rock band Creed, so my tastes are probably different than yours. Does this mean that all bets are off when it comes to implementing methods? The answer is both yes and no.
If you are the designer and programmer, you can do whatever you want. If you don't have to get paid for the code you write, you can go nuts. There are several good books on stylistic issues and rules of thumb. A recent book is Martin Fowler's refactoring book. Grady Booch, James Coplien, and Bjarne Stroustrup have written excellent books on object-oriented programming. You can find references to some of these in this book's bibliography. And, you can use the examples in this book.
Most of the examples in this book are refactored to some extent, but more importantly, this is the way I have been writing code for about 12 years now. I write the style of code presented in this book for a very good reason: After 10-plus years , a half dozen languages, and millions of lines of code, my particular style allows me to write very small amounts of highly reusable, maintainable code and do it very fast. Here are a few basic guidelines I follow when implementing methods.
These lessons weren't employed from day one. They were acquired from the masters of our time, over a period of time. Some of them, howeverlike short, singular methodsjust always seemed to make sense. Finally, the guideline can be illustrated with a quick story.
I purchased my first car for $325 when I was 15, the day I received my learner's permit. The term is rolling wreck. There was nothing beautiful about this car except for the idea of autonomy. It had four different- sized tires, a hole in the radiator the size of a football, the wrong transmission, and the front seat adjustment didn't lock. When the car stopped , the bench seat slid all the way forward; when the car accelerated, the seat slid all the way back.
Shortly after buying the car, I decided to start refurbishments. I chose to begin with the fan belt. I borrowed my dad's tools and began to try to remove the fan, followed by the water pump, and probably would have disassembled the engine block if I knew how, all to change the belt. After a couple of hours I gave up, put the few screws from the fan I was able to get out back in, and drove the car to a garage. The mechanic loosened the alternator, which eased the tension on the old belt. He slipped the belt off the alternator and over the fan, and placed the new belt on in reverse order. Finally, he increased the tension by adjusting the alternator and tightened the alternator bolts. The charge for about five minutes' labor was $35 bucks. Pretty steep for five minutes' work. He got 35 bucks, but not for the difficulty of the taskrather, he got paid because he knew how to perform the task. (Okay, I borrowed a little from an age-old fable.)
The moral is that knowing how and why is the only justification you need for making up your own reasons for deviating from general guidelines. Of course, if there is no justification, someone will challenge you or change your code when you aren't looking. Code is about as personal as any other creative activity. Following good rules for the general case will help your productivity; deviating will aid your genius.
Implementing Constructors and Destructors
There are two special methods you will need to implement. They are referred to as the constructor and destructor. A constructor is called to initialize a class, and a destructor is called to deinitialize, or finalize, a class. Visual Basic .NET implements the constructor as Sub New and the destructor as protected method Sub Finalize.
Every class gets at least one constructor inherited from the base class Object. Object defines a parameterless Sub New procedure that is called when you write code like the following:
Dim objectvariable As New classname
objectvariable represents any valid variable name and classname represents any valid reference type. From the statement, you might infer that New seems to be an operator, but if you step through a few examples, you will see that the statement proceeds directly to the Sub New() method. The New methodthe constructoris called when you create new instances of classes.
When an object is released from memory, the garbage collector calls the Sub Finalize method, the destructor. You can implement a destructor to deinitialize your objects by adding a Finalize method to your classes:
Protected Overrides Sub Finalize() End Sub
The garbage collection mechanism, or the garbage collector, is shortened to GC. GC is the namespace containing the garbage collector.
Traditionally, the purpose of a constructor is to release memory allocated to objects contained in a class. Because Visual Basic .NET employs a GC, you cannot be sure when the GC will actually call your destructor. You can still use a destructor to clean up objects contained in your classes, but if you have time-critical resources that need to be released, add a public Dispose method. By convention, we use a Public Sub Dispose() method to perform cleanup like closing files or recordsets.
Aggregation refers to adding members to a class. When your class has members that are themselves classes, and the class takes responsibility for creating and destroying instances of those members , we call this an aggregation relationship. When a class has members that are classes but some external entity creates and destroys those classes, we refer to this as an association relationship. When you define aggregation relationships in your class, you need to create a constructor for your class.
You can create a constructor for any reason, but you will need to create a constructor if you want to instantiateor, create an instance of a classaggregate members. The default constructor is represented by Sub New with no parameters. To demonstrate , I defined a LoaderClass that uses the System.IO.TextReader class to load a text file into an ArrayList.
Public Sub New() MyBase.New() FArrayList = New ArrayList() End Sub
The code will run correctly even if the MyBase.New() statement is removed. Semantically, all derived classes must call the parent constructor, but Visual Basic .NET seems to do it for you in most instances. Rather than trying to guess when and why Visual Basic .NET might call the base class's constructor, always place MyBase.New() as the first statement in your constructor. If you want to call a parameterized constructor in your child class, replace the empty-parameter constructor call with a parameterized constructor call.
The parameterless Sub New() calls the base class constructor using MyBase.New(). MyBase is a reserved word that allows you to refer to members in a class's base class, also called its parent class. The internal storage for the LoaderClass is an ArrayList. Because the LoaderClasswhose constructor is shownowns the ArrayList, the constructor instantiates the ArrayList before anything else happens.
Defining Overloaded Parameterized Constructors
If you need to pass external data into your class to ensure proper initialization, you can define a parameterized constructor.
The LoaderClass is designed to load a text file into an ArrayList. Thus it makes sense to initialize LoaderClass objects with a filename. By having two constructors we can create instances before we know the filename, or we can initialize loaders with a filename:
Public Sub New(ByVal AFileName As String) Me.New() FileName = AFileName End Sub
To avoid replicating code, we delegate part of the responsibility for class construction to the parameterless constructor with Me.New() and cache the filename parameter.
Having two Sub New methods in the same class means that you have overloaded the constructor. Constructors don't allow the use of the Overloads keyword. This is a special rule applied by the Visual Basic .NET engineers for constructors. (Refer to the section "Using Modifiers" later in this chapter for more on overloading methods.)
If you allocate memory in a constructor, you need to implement a destructor to deallocate the memory. Sub New is used similarly to Class_Initialize from VB6, and Sub Finalize is used similarly to Class_Terminate from VB6. Objects created in a constructor need to be disposed of in the destructor.
Generally, if your class doesn't define a constructor, you probably don't need a destructor. Here is the basic form of the Finalize destructor, as implemented in the LoaderClass example class:
Protected Overrides Sub Finalize() Dispose() End Sub
By implementing the destructor in terms of the Dispose method, you can call the Dispose method explicitly to release objects in advance of the GC. There are special considerations for cleaning up contained resources; refer to the next section for more on implementing a Dispose method.
The Finalize destructor is Protected; therefore, you cannot call it directly. The destructor is called by the GC. Here is a basic list of guidelines for implementing a destructor:
Because you don't know when the GC will release your objects, you can implement a Public Dispose method and call it explicitly in a Finally block of a resource protection block if you want deterministic finalization for things like file streams and threads.
Implementing a Dispose Method
Destructors are protected. Consumers cannot and shouldn't call the Finalize method directly. You can run the garbage collector explicitly by writing System.GC.Collect, but doing so isn't a recommended practice and incurs significant overhead.
If you want to clean up expensive objects, implement a Public Dispose method and call that. Here are some recommended practices for implementing a Dispose method:
Employing these guidelines, here is the Dispose method for the LoaderClass:
Public Sub Dispose() Implements IDisposable.Dispose Static FDisposed As Boolean = False If (FDisposed) Then Exit Sub FDisposed = True Close() FArrayList = Nothing GC.SuppressFinalize(Me) End Sub
The Dispose method implements IDisposable.Dispose (thus we know that Implements IDisposable is included in our LoaderClass). The static local variable is used to make Dispose safe to call more than once. The second statement toggles the Boolean, indicating that we have called Dispose. The LoaderClass.Close method is called to close the TextReader, not shown yet, and the ArrayList is assigned to Nothing. Finally, GC.SuppressFinalize tells the GC that it does not need to call Finalize for this object.
There are two keywords you will encounter when working with classes, MyBase and MyClass. MyBaseso you have already seenallows you to invoke methods in your class's base class that may be overloaded in your class, resolving any name ambiguity.
MyClass is roughly equivalent to the Me reference to self. MyClass assumes that any methods called with MyClass are declared as NotOverridable. MyClass calls a method without regard for the runtime type of the object, effectively bypassing polymorphic behavior. Because MyClass is a reference to an object, you can't use MyClass in Shared methods.
The reference to self Me was carried over in Visual Basic .NET. Me requires an instance to use. MyClass invokes a method in the same class, but Me invokes a method in the object actually referred to by Me, that is, in a polymorphic way. Consider the following classes.
Public Class Class1 Public Overridable Sub Proc() End Sub Public Sub New() Me.Proc() End Sub End Class Public Class Class2 Inherits Class1 Public Overrides Sub Proc() End Sub End Class
When an object of type Class2 is created, the constructor in Class1 invokes Class2.Proc. If Me.Proc is revised to MyClass.Proc, then Class1.Proc is invoked.
Adding Function and Subroutine Methods
Methods are simply functions and subroutines that are defined within the confines of a class (or Structure) construct. The only challenge in implementing methods is picking the right methods for your problem, keeping them simple, and determining the kind of access or modifiers that make sense for each method.
Unfortunately, how methods are defined is a subjective matter of style. The best way to learn to implement methods is to read and write a lot of code, and select a style that proves to work reliably for you. Don't hesitate to experiment and revise . Listing 7.6 contains the complete listing for LoaderClass.
Listing 7.6 The complete listing of the LoaderClass
1: Imports System.IO 2: 3: Public Class LoaderClass 4: Implements IDisposable 5: 6: Private FFileName As String 7: Private FReader As TextReader 8: Private FArrayList As ArrayList 9: Public Event OnText(ByVal Text As String) 10: 11: Private Sub DoText(ByVal Text As String) 12: RaiseEvent OnText(Text) 13: End Sub 14: 15: Public Property FileName() As String 16: Get 17: Return FFileName 18: End Get 19: Set(ByVal Value As String) 20: FFileName = Value 21: End Set 22: End Property 23: 24: Public Overloads Sub Open() 25: If (FReader Is Nothing) Then 26: FReader = File.OpenText(FFileName) 27: Else 28: Throw New ApplicationException("file is already open") 29: End If 30: End Sub 31: 32: Public Overloads Sub Open(ByVal AFileName As String) 33: FileName = AFileName 34: Open() 35: End Sub 36: 37: Public Sub Close() 38: If (FReader Is Nothing) Then Exit Sub 39: FReader.Close() 40: FReader = Nothing 41: End Sub 42: 43: Private Function Add(ByVal Text As String) As Boolean 44: If (Text = "") Then Return False 45: DoText(Text) 46: FArrayList.Add(Text) 47: Return True 48: End Function 49: 50: Private Function Reading() As Boolean 51: Return Not FDisposed AndAlso Add(FReader.ReadLine()) 52: End Function 53: 54: Public Sub Load() 55: While (Reading()) 56: Application.DoEvents() 57: End While 58: End Sub 59: 60: Public Sub New() 61: MyBase.New() 62: FArrayList = New ArrayList() 63: End Sub 64: 65: Public Sub New(ByVal AFileName As String) 66: Me.New() 67: FileName = AFileName 68: End Sub 69: 70: Private FDisposed As Boolean = False 71: 72: Public Sub Dispose() Implements IDisposable.Dispose 73: If (FDisposed) Then Exit Sub 74: FDisposed = True 75: Close() 76: FArrayList = Nothing 77: End Sub 78: 79: Protected Overrides Sub Finalize() 80: Dispose() 81: MyBase.Finalize() 82: End Sub 83: 84: Public Shared Function Load(ByVal AFileName _ 85: As String) As LoaderClass 86: 87: Dim ALoader As New LoaderClass(AFileName) 88: ALoader.Open() 89: ALoader.Load() 90: Return ALoader 91: 92: End Function 93: End Class
The LoaderClass defines a DoText method, two Open methods, Close, Add, Reading, and Load methods. DoText was implemented as a Private method; all it does is raise the event to notify any objects that might want to eavesdrop on the loading process. The Open methods are both Public; they were defined with the Overloads modifier to allow consumers to open a file with a passed filename argument, or, without, assuming that the filename was already assigned in the constructor call or by modifying the property value. (The FileName property is defined on lines 15 through 22.) The Add method eliminates the need for a temporary. We can pass the result of the TextReader.ReadLine method as a parameter to Add and return a Boolean indicating whether we want the value or not. The Reading function returns a Boolean indicating that we were able to add the text and we haven't called the Dispose method (see lines 50 to 52). Line 51 demonstrates how to use the short-circuiting AndAlso operator. If Dispose has been called, Reading short circuits on Not Disposed. Otherwise, the next line of text is read. If FReader.ReadLine has read the entire file, the ReadLine method returns an empty string ("") and Reading returns False. Because we separated the Reading and Add behavior out, the Load method is a very simple while loop (lines 54 to 58).
Open, Close, and Load are the only public methods keeping the LoaderClass pretty easy to use for consumers. Everything else supports the three public methods and wouldn't make sense for consumers to call directly, so all the other methods are Private.
Consider a single musician versus an orchestra. If you have one musician and one instrument, the kind of music and the orchestration of that instrument are severely limited. If you have an entire orchestra with dozens of musicians and instruments, the orchestration and subsequently the variety of music you can play are significantly increased.
Monolithic methods are like the lone musician: enjoyable, but not very diverse. Many singular methodsthink of overloaded constructors as an entire wind sectionmean that each method can specialize and be reorchestrated in a greater variety.
In this particular example, we probably won't get a lot of code reuse out of Add, Reading, and DoText. What you will get are singular functions that are very easy to implement, and it's very unlikely that they will introduce errors. One more benefit that might not be obvious is that methods can be overridden in subclasses. (The private methods in LoaderClass would need to be made protected if we decided to extend them in a subclass, but we could do so with very little effort.) If you use a small number of monolithic methods, you have fewer opportunities for overriding and extending behavior.
Using Method Modifiers
Method modifiers enable us to exercise a greater, more verbose control over our method implementations .
The Overloads modifier is used to indicate that two or more methods in the same class are overloaded. The LoaderClass.Open methods demonstrate method overloading.
The benefit of method overloading is that it allows you to implement methods that support the same semantic operation but differ by argument number or type. Consider a method that writes text to the console, named Print. Without method overloading, you would have to implement a uniquely named method for printing any data type, for example, PrintInteger, PrintString, PrintDate. Such an approach would require that users learn the names of many methods that perform the same behavior. With overloading you could name all of the Print methods and let the compiler resolve which method to call based on the data type; offload tedious work to the compiler and let the programmer think about important things.
The procedure heading, number and type of arguments, and return type (if the procedure is a function) comprise the procedure's signature.
Properties can be overloaded too.
Here are the basic rules for using the Overloads modifier:
If you find yourself implementing operations because the data type is different but the kind of operation is the same, you do need overloaded methods. If you find yourself implementing two methods with the same code, but differing only by the value of one or more parameters, you need one method with an Optional parameter.
The Overrides modifier supports polymorphism. You use the Overrides modifier when you want to extend or change the behavior of a method in a base class. The base class method must have the same signature as the method overriding it.
We will revisit the Overrides modifier in Chapter 10.
Overridable, MustOverride, and NotOverridable Modifiers
The Overridable, MustOverride, and NotOverridable modifiers are used to manage which methods can be overridden and which must be overridden.
The Overridable and NotOverridable modifiers are mutually exclusive. The Overridable modifier indicates that a method can be overridden. The NotOverridable modifier indicates that you cannot override a method. The MustOverride modifier indicates that a method is abstract, and child classes must implement the MustOverride methods in a parent class. MustOverride implies that a method is overridable, so you don't need the Overridable modifier and NotOverridable doesn't make sense for MustOverride methods.
MustOverride methods have no implementation in the class where they are declared with this modifier. MustOverride methods are equivalent to purely virtual methods in C++ and virtual abstract methods in Object Pascal; descendants must implement these methods.
Using the Shadows Modifier
If you want a child class to use a name previously introduced in a parent class, use the Shadows keyword to do so. Shadowed names aren't removed from the parent class; the Shadows keyword simply allows you to reintroduce a previously used name in the child class without a compiler error.
The member in the child class doesn't have to be the same type as the shadowed member; the two members only need identical names. Any kind of member in a child class can shadow any kind of member in a parent class. A method in a child class can shadow a field, property, method, or event in a parent class. The following fragment demonstrates using the Shadows modifier to reintroduce a method in a child class with the same name as a method in the parent class.
Public Class A Public Sub Foo() End Sub End Class Public Class B Inherits A Public Shadows Sub Foo() End Sub End Class
The listing shows two classes, A and B. B is subclassed from A; that is, A is B's parent. Both A and B have a method Foo. The introduction of the Shadows keyword on B.Foo means that B.Foo hides A.Foo. If you have identical names in two classes related by inheritance, you must use either the Shadows or Overrides modifier. (Refer to Chapter 10, "Inheritance and Polymorphism," for more details on using Shadows and Overrides.)
Shared members are accessible without creating instances of reference types or value typesclasses or structures, respectively. Chapter 11 discusses shared members in detail.
Using Access Specifiers
Classes support the sage advice divide et impera, divide and conquer. A class allows you to think like a producer when defining a class and focus only on the implementation of the class. When you are using the class, you become a consumer of the class. Your only considerations as a class consumer are the public members, and both the public and protected members if you are implementing a child class.
The benefit of access modifiers is that as a consumer you don't ever have to worry about the private members and usually don't have to worry about the protected members. If you follow the general guideline to keep public members to a half dozen or so, as a class consumer you have effectively offloaded most of the implementation detailsthe private and protected membersof the class. That is to say, you have divided the problem into a simpler problem: understanding the class as a producer when you are writing the class and concerning yourself with only the public members as a consumer. This division of focus facilitates managing the complexity of a growing body of code. By aggressively managing code, using access specifiers to limit what consumers must comprehend, you can make your code easier to manage.
You can use Public, Protected, Private, Friend, and Protected Friend access specifiers on methods. Refer to "Using Class Access Specifiers" at the beginning of the chapter for a description of how each access specifier affects entities. The following list briefly reviews the impact of access specifiers on methods:
Hundreds of examples are demonstrated throughout this book, including several in this chapter. Refer to the first section of this chapter, "Defining Classes," for general guidelines covering the number of methods and the employment of access specifiers.