Defining Nested Classes A nested class is quite simply a class defined within a class. Nested classes can be defined with all of the elements of any other class. Here's the skeletal code for a nested class: Public Class Outer Public Class Nested End Class End Class Your motivation for creating nested classes is to collect a group of elements that make sense together and only within the containing class. For example, if you have a class and some fields, properties, and methods that define a concept separate but a part of the containing class and you want to store multiple variations of state, you might implement a nested class. In general you won't need to implement nested classes. Any implementation that can be done with nested classes can be done without nested classes. If you are interested, you can look into the advanced idioms, including the letter-envelope idiom, discussed by James Coplien in Advanced C++ . Listing 7.7 provides an example of nested classes, demonstrating the grammatical technique. Listing 7.7 Multiple nested classes, inheritance, and multithreading in the TrafficLight.sln demo program1: Public Class Signal 2: #Region " Public Members " 3: Public Event OnChange(ByVal sender As System.Object, _ 4: ByVal e As System.EventArgs) 5: 6: Public Sub Draw(ByVal Graphic As Graphics) 7: Graphic.FillRectangle(Brushes.Brown, Rect) 8: DrawLights(Graphic) 9: End Sub 10: 11: Public Sub New(ByVal Rect As Rectangle) 12: FRect = Rect 13: PositionLights() 14: TurnOnGreen() 15: CreateThread() 16: End Sub 17: 18: Public Sub Dispose() 19: Static Done As Boolean = False 20: If (Done) Then Exit Sub 21: Done = True 22: KillThread() 23: GC.SuppressFinalize(Me) 24: End Sub 25: 26: Public Sub NextSignal() 27: While (FThread.IsAlive) 28: FThread.Sleep(SleepTime()) 29: ChangeState() 30: DoChange() 31: End While 32: End Sub 33: 34: #End Region 35: 36: #Region " Protected Members " 37: 38: ' Destructor 39: Protected Overrides Sub Finalize() 40: Dispose() 41: End Sub 42: 43: Protected Sub CreateThread() 44: FThread = New Threading.Thread(AddressOf NextSignal) 45: FThread.IsBackground = True 46: StartSignal() 47: End Sub 48: 49: Protected Sub StartSignal() 50: FThread.Start() 51: End Sub 52: 53: Protected Sub StopSignal() 54: FThread.Abort() 55: FThread.Join() 56: End Sub 57: 58: Protected Sub KillThread() 59: StopSignal() 60: FThread = Nothing 61: End Sub 62: 63: Protected Sub DrawLights(ByVal Graphic As Graphics) 64: Dim I As Integer 65: For I = 0 To FLights.GetUpperBound(0) 66: FLights(I).Draw(Graphic) 67: Next 68: End Sub 69: 70: #End Region 71: 72: #Region " Private Members " 73: Private FLights() As Light = _ 74: {New GreenLight(), New YellowLight(), New RedLight()} 75: Private FRect As Rectangle 76: Private FThread As Threading.Thread 77: 78: Private ReadOnly Property Green() As Light 79: Get 80: Return FLights(0) 81: End Get 82: End Property 83: 84: Private ReadOnly Property Yellow() As Light 85: Get 86: Return FLights(1) 87: End Get 88: End Property 89: 90: Private ReadOnly Property Red() As Light 91: Get 92: Return FLights(2) 93: End Get 94: End Property 95: 96: Private ReadOnly Property Rect() As Rectangle 97: Get 98: Return FRect 99: End Get 100: End Property 101: 102: Private Sub PositionLights() 103: 104: Dim I As Integer 105: For I = 0 To FLights.GetUpperBound(0) 106: FLights(I).Rect = GetRect(I) 107: Next 108: 109: End Sub 110: 111: Private Sub TurnOnGreen() 112: Green.State = True 113: End Sub 114: 115: Private Sub DoChange() 116: RaiseEvent OnChange(Me, Nothing) 117: End Sub 118: 119: 120: Private Function GetRect(ByVal Index As Integer) As Rectangle 121: Return New Rectangle(Rect.Left + 10, _ 122: Rect.Top + 10 + CInt(Math.Round((2 - Index) / 3 * Rect.Height)), _ 123: Rect.Width - 20, CInt(Math.Round(Rect.Height / 3)) - 20) 124: End Function 125: 126: Private Sub ChangeState() 127: Static Current As Integer = 0 128: Current = (Current + 1) Mod 3 129: Dim I As Integer 130: 131: For I = FLights.GetLowerBound(0) To _ 132: FLights.GetUpperBound(0) 133: FLights(I).State = I = Current 134: Next 135: End Sub 136: 137: Private Function SleepTime() As Integer 138: Static T() As Integer = {1000, 5000} 139: If (Yellow.State) Then 140: Return T(0) 141: Else 142: Return T(1) 143: End If 144: End Function 145: 146: #End Region 147: 148: 149: #Region " Nested Members " 150: 151: ' Virtual Abstract Nested Light Class 152: ' Base class for the signal light classes 153: 154: Private MustInherit Class Light 155: Public State As Boolean = False 156: Public Rect As Rectangle 157: 158: Protected MustOverride ReadOnly Property _ 159: BrushArray() As Brush() 160: 161: Protected Function GetBrush() As Brush 162: If (State) Then 163: Return BrushArray(1) 164: Else 165: Return BrushArray(0) 166: End If 167: End Function 168: 169: Public Sub Draw(ByVal Graphic As Graphics) 170: Graphic.FillEllipse(GetBrush(), Rect) 171: End Sub 172: 173: Protected Overrides Sub Finalize() 174: Rect = Nothing 175: End Sub 176: End Class 177: 178: ' RedLight nested class, inherits from Light 179: Private Class RedLight 180: Inherits Light 181: 182: Protected Overrides ReadOnly Property BrushArray() As Brush() 183: Get 184: Static Brush() As Brush = {Brushes.Gray, _ 185: Brushes.Red} 186: Return Brush 187: End Get 188: End Property 189: End Class 190: 191: ' YellowLight nested class, inherits from Light 192: Private Class YellowLight 193: Inherits Light 194: 195: Protected Overrides ReadOnly Property BrushArray() As Brush() 196: Get 197: Static Brush() As Brush = {Brushes.Gray, _ 198: Brushes.Yellow} 199: Return Brush 200: End Get 201: End Property 202: 203: End Class 204: 205: ' GreenLight nested class, inherits from Light 206: Private Class GreenLight 207: Inherits Light 208: Protected Overrides ReadOnly Property BrushArray() As Brush() 209: Get 210: Static Brush() As Brush = {Brushes.Gray, _ 211: Brushes.Green} 212: Return Brush 213: End Get 214: End Property 215: End Class 216: #End Region 217: 218: End Class Because the listing is very long, the elaboration is subdivided into individual subsections. Each of the following subsections elaborates on a specific aspect of Listing 7.7. Understanding the Motivation for Nested ClassesThe motivation for using nested classes at all is questionable, because any class that can be nested could technically be implemented as a non-nested class. There might be some strategic reasons for implementing nested classes, but you will find them few and far between. To demonstrate the technical aspects of implementing nested classes, the TrafficSignal.sln sample program was contrived. As a suggestion, the traffic signal lightsred on top, yellow in the middle, and green on the bottommight make sense as nested classes. You are unlikely to encounter a need for a single-signal light that maintains state relative to other lights without the other lights. (You could have a single blinking yellow light, but you could implement that as an element of a different traffic signal. However, this does illustrate what you are likely to encounter when using nested classes: Each time you implement a nested class, you are likely to find an exception that suggests the class might be better off not nested.) The Signal contains three instances of a Light class. Because the Lights only make sense in the context of the entire signalor so we assume for our purposesand we need more than one light, we implement the light as a nested class and create three instances of the light. Defining the Signal ClassThe Signal class represents a three-state , red-yellow-green traffic light. The signal will be defined to maintain its state and draw itself on a device context, in a Visual Basic .NET Graphics object. The Signal class will have an array of three Light objects and a means of changing state in an ordered way from Green to Yellow to Red. Two signals at different signal-states are shown in Figure 7.4. Figure 7.4. Two instances of the Signal class from TrafficSignal. sln.
All of the members of the Signal class are defined on lines 1 through 146 of Listing 7.7. The subsections that follow elaborate on the different aspects of the Signal class. Using Code Outlining to OrganizeThe first thing you will notice about Listing 7.7 is that it employs code outlining using the #Region pragma. A long, complicated class listing like the Signal class will benefit from a little extra housekeeping. The top half of the class contains the members of the Signal class, and the bottom half, starting on line 149, contains the nested classes. By adding #Region pragmas, you can collapse and expand whole subsections of your code to make development easier. A reasonable practice to employ is to list members by priority. Consumers will only be interested in Public members, so Public members are listed first. Generalizers will be interested in Protected members too, so those are listed after the Public members. Finally, Private members are only of interest to a producerthe person who implements the class; thus Private members are listed last. Tip A little housekeeping helps, but if the class is relatively simple, the members are added as they evolve , in no particular order. By their sheer existence, nested members are the least significant members of a class. If the nested classes were significant to consumers, they wouldn't be nested; consequently, nested classes are listed last. Defining the Constructor and DestructorThe Signal class has three instances of the Light class and a Thread object. For this reason, a constructor, destructor, and Dispose methods were added to the Signal class. The constructor and the Dispose method are defined on lines 11 to 16 and 18 to 24, respectively. The excerpt from Listing 7.7 is shown in Listing 7.8 for convenience. The Protected Finalize method simply calls the Dispose methodlines 39 to 41and isn't repeated here. Listing 7.8 The constructor and Dispose methods of the Signal class11: Public Sub New(ByVal Rect As Rectangle) 12: FRect = Rect 13: PositionLights() 14: TurnOnGreen() 15: CreateThread() 16: End Sub 17: 18: Public Sub Dispose() 19: Static Done As Boolean = False 20: If (Done) Then Exit Sub 21: Done = True 22: KillThread() 23: GC.SuppressFinalize(Me) 24: End Sub The constructor Sub New delegates all of its tasks to well-named methods. The names of the methods tell you what is happening. Another strategy is to add an Initialize method and delegate all initialization code to that method; this technique allows you to reinitialize an object without constructing a new object. The constructor clearly indicates that the bounding Rectangle is being cached to a Field, the Lights are positioned, the green light is turned on, and the Thread is created. (Perhaps RunThread would more clearly indicate that the thread is created and running after line 15 runs.) The Dispose method uses a static local variable to act as a sentinel. After Dispose is called one time, this method becomes a noop method. All Dispose does is kill the thread that controls the light states and suppress garbage collection. The lights themselves aren't released because they were created outside of the constructor on lines 73 and 74 with an array initializer. Creating the Signal ThreadLine 15 (see Listing 7.7 or 7.8) calls the CreateThread method. All of the thread-management methods are shown in Listing 7.9, which is an excerpt from Listing 7.7. Listing 7.9 The thread-management methods of the Signal class43: Protected Sub CreateThread() 44: FThread = New Threading.Thread(AddressOf NextSignal) 45: FThread.IsBackground = True 46: StartSignal() 47: End Sub 48: 49: Protected Sub StartSignal() 50: FThread.Start() 51: End Sub 52: 53: Protected Sub StopSignal() 54: FThread.Abort() 55: FThread.Join() 56: End Sub 57: 58: Protected Sub KillThread() 59: StopSignal() 60: FThread = Nothing 61: End Sub Line 44 creates an instance of the System.Threading.Thread class. The Thread class constructor takes the AddressOf of a procedure; this procedure is where the thread will fork when the thread is started. Line 45 indicates that the thread is a background thread, allowing the application to quit and stop the signal threads. Line 46 calls StartSignal, which turns the traffic signal on by starting the subroutine on line 49. When Dispose is called, Dispose calls KillThread. From the listing you can determine that KillThread calls StopSignal and releases the thread object by assigning it to Nothing. StopSignal calls the FThread.Abort method and FThread.Join waits for the thread to actually stop executing. FThread.Abort kills the thread by raising an uncatchable ThreadAbortException; the exception allows all of the Finally blocks in the thread to execute, so you need to call Join to ensure that the thread has completed. Multithreading is brand new in Visual Basic .NET. If you haven't programmed in a multithreaded language before, threads might be a challenge to you. The Thread class and multithreaded programming in Visual Basic .NET are covered in detail in Chapter 14, "Multithreaded Applications." Drawing the Signal Using an EventSignal objects are drawn on whatever control passes its Graphics object to the Signal.Draw method. (In reality the signal should be a control, but we would have to digress too far from our discussion of nested classes to get there from here.) Consider these code excerpts: 6: Public Sub Draw(ByVal Graphic As Graphics) 7: Graphic.FillRectangle(Brushes.BurlyWood, Rect) 8: DrawLights(Graphic) 9: End Sub ... 63: Protected Sub DrawLights(ByVal Graphic As Graphics) 64: Dim I As Integer 65: For I = 0 To FLights.GetUpperBound(0) 66: FLights(I).Draw(Graphic) 67: Next 68: End Sub ... 161: Protected Function GetBrush() As Brush 162: If (State) Then 163: Return BrushArray(1) 164: Else 165: Return BrushArray(0) 166: End If 167: End Function 168: 169: Public Sub Draw(ByVal Graphic As Graphics) 170: Graphic.FillEllipse(GetBrush(), Rect) 171: End Sub Drawing is performed in three methods. Signal.Draw on lines 6 to 9 simply draws the Signal as a Rectangle, and calls Signal.DrawLights to draw each of the contained lights. DrawLights iterates over each light and calls the light's draw methods, demonstrating the division of labor offered by classes and nested classes in this instance. Light.Draw simply calls Graphics.FillEllipses (using the argument Graphic), using each object's cached Rect as the bounding region of the light and the Light.GetBrush method to determine the brush based on the state of the light. When the drawing method is invoked it only cares about Ellipses, Brushes, and Rects, not states and calculating bounding regions . Line 163 and 165 (see Listing 7.7 or 7.9) uses the polymorphic property BrushArray to return the correct array of brush objects based on the instance of the lamp. If State = False, the off brush is returned; if State = True, the on brush is returned. Chapter 10 offers an extended discussion on inheritance relationships and how polymorphic behavior like the BrushArray() property works, as well as a discussion on defining and implementing interfaces. Defining the Abstract Base Class, LightLight, GreenLight, YellowLight, and RedLight are all nested classes. Light uses the MustInherit class modifier. The Light class demonstrates abstract base classes in Visual Basic .NET, which means that Light is intended to be descended from but never directly created. Light has to use MustInherit because it declares an abstract property: Protected MustOverride ReadOnly Property BrushArray() As Brush() Notice that BrushArray from lines 158 and 159 of Listing 7.7 has no procedure block; it's a declaration only. Statements with MustOverride refer to virtual and abstract members, which means they must be implemented in a descendant class. Virtual, abstract members are analogous to interface declarations in COM classes. GreenLight, YellowLight, and RedLight must each implement the property BrushArray or they will be virtual and abstract. Declarative statements are used to guide the implementation of descendant classes. The way Light is implemented, its Draw method depends on GetBrush, and GetBrush, in turn , depends on being able to get the correct brush and color based on the state of a specific light. In structured languages, or incorrectly in object-oriented languages, this kind of behavior was implemented using a case statement. Implementing the Signal LightsThe signal light classes themselves are easy to implement. Each specific light need only inherit from Light and implement the read-only property BrushArray. Listing 7.10 demonstrates the RedLight class only. (All of the lights are identical except for the values used to initialize the BrushArray.) Listing 7.10 Implementation of one of the signal lights178: ' RedLight nested class, inherits from Light 179: Private Class RedLight 180: Inherits Light 181: 182: Protected Overrides ReadOnly Property BrushArray() As Brush() 183: Get 184: Static Brush() As Brush = {Brushes.Gray, _ 185: Brushes.Red} 186: Return Brush 187: End Get 188: End Property 189: End Class Line 182 implements a protected, overridden, read-only property named BrushArray. A Static array of brushes is initialized to global brushes defined in the Brushes object. The RedLight class uses gray- and red-colored brushes. There are several ways we could have implemented the colored signal lights. We could have used one Light class and a factory method that initializes the brush values based on the kind of light; kind could have been implemented using an enumeration. In software, as in all things, you eventually have to pick a direction and run with it. I specifically picked this implementation strategy to demonstrate nested classes, but I do like the way it turned out. A Final Word on the Demo ProgramIn a production system, the traffic signal sample program would better serve an application as a component. If you were modeling a city's traffic lights, you would want to rethink the threads, too. In a city the size of Okemos, you might have 30 threads, but in Chicago you would blow up the computer switching all of the necessary threads. Additionally, the signal lights would need to be coordinated. A better implementation might be one threaded manager that coordinates lights at opposing intersections and manages the timing from block to block. Finally, use better graphics if you are going to simulate the visual aspects of traffic management, or anything else for that matter. |
Team-Fly |
Top |