isual Basic developers had wanted inheritance for what seems like decades. The feature finally made it into VB.NET, making VB.NET an official object-oriented language. By now you are probably already using inheritance in your applications, but are you fully taking advantage of its potential?
As a consultant, I am frequently called into companies to perform code reviews of VB.NET and C# code. I truly enjoy this process because I have the benefit of seeing all of the many ways that applications can be architected and implemented. In going through the code, I have seen great use?and abuse?of inheritance in .NET applications.
This article reviews what inheritance is, when and how to use it, and provides some tips for getting the greatest benefit from this valuable feature.
Inheritance is an abstraction for sharing similarities among classes while preserving their differences. It is easiest to fully grasp inheritance by way of an example.
This example is a simple simulation that is loosely based on “WA-TOR”, which was first introduced in a famous Scientific American magazine article by A. K. Dewdney. WA-TOR is a planet composed entirely of an ocean. It is shaped like a donut (the technical term for which is “toroid” – if you are into video games, think Halo).
Though it is shown on a two-dimensional grid (see Figure 1) the movement is such that if something moves up off the top it shows up at the bottom, if it moves right off the edge it shows up on the left edge, and so on.
As with any object-oriented application, start by thinking about the entities or objects involved with the application. For example, this simulation application has an Ocean entity that represents the ocean space. This is represented with an Ocean class that manages a two-dimensional grid:
|Figure 1. Simulated Movement: The WA-TOR simulation shows the movement of fish and sharks in a toroidal ocean.
Public Class Ocean Private myCell(0, 0) As Object Public Function getObject(ByVal p As Point) _ As Object Return myCell(p.x, p.y) End Function Public Sub putObject(ByVal thing As Object) myCell(thing.Location.x, thing.Location.y)= _ thing End Sub ... End Class
|Author’s Note: This simulation example does not provide guidance on good naming conventions.
The Ocean class also provides the functions defining the rules for movement within the ocean. For example, the code to move up from a point is as follows:
Public Function northOf(ByVal p As Point) As Point Dim y As Integer = p.y - 1 If (y
Using this as an example, you can easily write the code to move left, right, and down.
In this simulation, there are three types of entities that can exist within the ocean: water, fish, and sharks (see Figure 1). Each of these entities will reside within a cell of the two-dimensional ocean grid. So each of these entities needs a property to identify its location, defined with an (x, y) point, and an image defining how the entity will appear visually on the grid. These entities are implemented as classes in the application with Location and Image properties. As an example, the Water class is as follows:
Public Class Water Private myLocation As Point Private Shared myImage As Image _ = Image.FromFile("../water.jpg") Public ReadOnly Property image() As Image Get Return myImage End Get End Property Public Property location() As Point Get Return myLocation End Get Set(ByVal Value As Point) myLocation = Value End Set End Property Public Sub New(ByVal location As Point) myLocation = location End Sub End Class
This code defines the two properties and a constructor for creating a new water entity at a particular point in the ocean grid.
You can copy and paste this code to create a very similar looking Fish class. Using a copy and paste technique to share similarities between classes is jokingly referred to as "clipboard inheritance." The resulting Fish class is as follows:
Public Class Fish Private myLocation As Point Private Shared myImage As Image _ = Image.FromFile("../fish.jpg") Public ReadOnly Property image() As Image Get Return myImage End Get End Property Public Property location() As Point Get Return myLocation End Get Set(ByVal Value As Point) myLocation = Value End Set End Property Public Sub New(ByVal location As Point) myLocation = location End Sub End Class
Unlike the Water class, however, the Fish class needs an additional method. For the simulation to work properly, the Fish need to move. The movement rules for the fish in this simulation are that a fish looks in a random direction for an empty location. If the location contains only water, the fish spawns a new fish at its current location and moves into the empty location. If the location contains something else, the fish stays in its current location. This Move method is as follows:
Public Overrides Sub Move(ByVal newOcean As Ocean) Dim n As Integer = Rnd.Next(1, 5) Dim p As Point Select Case n Case 1 p = newOcean.northOf(myLocation) Case 2 p = newOcean.eastOf(myLocation) Case 3 p = newOcean.southOf(myLocation) Case 4 p = newOcean.westOf(myLocation) End Select If (newOcean.getObject(p).GetType. _ Equals(GetType(Water))) Then newOcean.putObject(New Fish(myLocation)) myLocation = p newOcean.putObject(Me) Else newOcean.putObject(Me) End If End Sub
You can then repeat the "clipboard inheritance" process to define the properties in a Shark class. But the fact that the property code was repeated in all three classes should give you an indication that you have similarities among these classes. And sharing similarities is part of what inheritance is all about.
I Want My Inheritance
Looking at the Water and Fish classes, it is easy to see some class commonalities. Inheritance involves extracting that commonality into a separately defined class. That new class is called a superclass, parent class or base class. The original classes then inherit from the new base class and become child classes, which are also referred to as derived classes or subclasses.
In this example, you can define a new base class named OceanElement that defines any element that can be placed into the WA-TOR ocean. Both the location and image properties from the child classes are extracted from those classes and instead implemented in the OceanElement class:
Public MustInherit Class OceanElement Protected myLocation As Point Public Property Location() As Point Get Return myLocation End Get Set(ByVal Value As Point) myLocation = Value End Set End Property Public MustOverride ReadOnly Property Image() _ As Image End Class
Notice the MustInherit keyword on the Class declaration. This keyword identifies the class as an abstract class. An abstract class is one that defines properties and methods but cannot itself be instantiated. This keyword is frequently used in base classes when the base class defines common functionality but does not itself represent an object in the application.
The Protected keyword in the declaration for the location ensures that the location value can only be accessed through the inheritance hierarchy. This means that only classes that inherit from this class or inherit from classes that inherit from this class can access that value directly. All other classes must access the value through the defined Property statements.
The MustOverride keyword in the declaration for the Image property denotes that the child classes must override this property. This keyword is needed in this case because each child class defines its own Image object containing its visual representation for the simulation.
The Water, Fish, and Shark classes all then inherit from this OceanElement base class. The Water class is shown as an example:
Public Class Water : Inherits OceanElement Private Shared myImage As Image _ = Image.FromFile("../water.jpg") Public Overrides ReadOnly Property image() _ As Image Get Return myImage End Get End Property Public Sub New(ByVal location As Point) myLocation = location End Sub End Class
Notice how much less code is here than in the earlier example of the Water class. The Image Property statement overrides the Image property implemented in the base class to define the unique image for the Water class. The constructor for the Water class sets the myLocation variable, which is now maintained by the base class. The Water class has access to this variable from the base class because it was defined in the base class with Protected scope.
As you have just seen, inheritance provides for sharing similarities among classes. But inheritance also provides for preserving differences between them.
The Fish and Shark classes share the same properties as the Water class. They could also inherit directly from the OceanElement class, and the result would look very similar to the Water class shown in the prior code snippet.
However, there is a key difference between the Water class and the Fish and Shark classes: the water does not move during the simulation but the fish and sharks do.
To preserve this difference, define a separate SeaCreature class. The SeaCreature class inherits from OceanElement so that it shares the Image and Location properties, but then extends the OceanElement class by adding a Move method and a Random property. The result is as follows:
Public MustInherit Class SeaCreature Inherits OceanElement Public Overridable Sub Move(ByVal newOcean _ As Ocean) newOcean.putObject(Me) End Sub Protected Shared Rnd As Random = _ New Random(Now.Millisecond) End Class
The Fish and Shark classes then inherit from the SeaCreature class as shown with the Fish class:
Public Class Fish : Inherits SeaCreature Private Shared myImage As Image = _ Image.FromFile("../fish.jpg") Public Overrides ReadOnly Property Image() _ As Image Get Return myImage End Get End Property Public Overrides Sub Move( _ ByVal newOcean As Ocean) Dim n As Integer = Rnd.Next(1, 5) Dim p As Point Select Case n Case 1 p = newOcean.northOf(myLocation) Case 2 p = newOcean.eastOf(myLocation) Case 3 p = newOcean.southOf(myLocation) Case 4 p = newOcean.westOf(myLocation) End Select
If (newOcean.getObject(p).GetType. _ Equals(GetType(Water))) Then newOcean.putObject(New Fish(myLocation)) myLocation = p newOcean.putObject(Me) Else MyBase.Move(newOcean) End If End Sub Public Sub New(ByVal location As Point) myLocation = location End Sub End Class
? Figure 2. WA-TOR Class Heirarchy: Sharks and Fish are SeaCreatures; SeaCreatures and Water are OceanElements. This defines the hierarchy of classes for the WA-TOR simulation.
By inheriting from the SeaCreature class, the Fish and Shark classes get all of the properties of the OceanElement class and the properties and methods of the SeaCreature class. This hierarchy of classes is shown in Figure 2.
Inheritance In Reality
Developing simulations is fun, they map well to the concepts of inheritance, and they're great for presentations because they are so visual. But most of us don't write simulation applications. In reality, most of us write business applications. That is when it gets easy to both use and abuse inheritance.
Let's look at the abuse first. There are several situations where it may not make sense to use inheritance.
- If all properties and methods in a base class are defined with MustOverride, then the base class provides no default functionality. In this case, you are better off using an interface. By using an interface, you also gain flexibility because you can define more than one interface on a class. Whereas you can only define one base class for a class. For example, if you want to ensure that all of your business objects (BO) have Retrieve, Validate, and Save methods, you can create a BO interface and implement that interface in each business object instead of creating a BO base class.
- If the base class provides a set of general functions that are not semantically related to the child classes, then the base class is really a library and not a base class. For example, if you have a set of logging features that are used by all of your business objects, create a Logging class with shared methods and call the methods as needed from your business objects. This Logging class can then be part of a library that is reusable in other applications.
- If the inheritance hierarchy is more than five classes deep, it becomes more difficult to manage the set of properties and methods provided by all of the class up the inheritance hierarchy. This can lead to errors and inappropriate behavior.
As an example of the last point, I spoke with a developer from Australia that implemented a deep hierarchy of classes for an army simulation program that he was writing. The simulation had many types of classes in the hierarchy to define different types of aircraft, vehicles, artillery, and individual troops. After the army reviewed the simulation, being Australia they thought it would be more realistic to add kangaroos to field. So one of the developers simply added another subclass on the deep hierarchy under the soldier class because it contained the functionality for initialization and movement. Wasn't he surprised when he ran the simulation and the kangaroos shot back! It is easy to forget to override all of the appropriate properties and methods when your hierarchy is too deep.
To do inheritance well, you should first look at the entities involved with your application. Then define the commonalities among the classes along with their differences. From that point, you can define a semantically correct inheritance hierarchy and implement the appropriate classes for that hierarchy.
But this is often easier said than done. Agile development processes are very popular these days. This means that you most likely don't know all that your application will need to do when you start architecting and building the application. Rather, you begin with the features that you are implementing first, develop your code so that it is easy to maintain, then you later add features based on their defined priority.
Using an agile approach, you don't always know all of the classes in your application. This makes it difficult to define the commonality between the classes. For this reason, you may find that your first implementation of your application may not have any inheritance relationships. As you continue to add and enhance classes over time, you will recognize the commonality between the classes. It is then that you will want to refactor the classes to use inheritance.
For example, let's say we are developing an invoicing application for a consulting company using an agile approach. The first iteration would include the basic invoicing features, such as the ability to generate an invoice based on each employee's time. The first iteration could define classes for Employee, Project, Timesheet, and Invoice.
For the second iteration, the users defined the need for a pre-pay invoice. We find much commonality between the time-based invoice and pre-pay invoice, so we refactor the Invoice class creating a base Invoice class and two child classes for time-based and pre-pay.
For the third iteration, the users defined the need to bill for subcontracted labor as well. Again we refactor, this time creating a Person base class with Employee and Subcontractor child classes.
The reality of software development does not always allow us to clearly create the inheritance hierarchy up front. Rather, inheritance relationships are often defined as the application is implemented and as it is enhanced and modified over time.
The important thing to remember about inheritance is that its entire purpose is to provide an easy way for your application to share similarities between classes while still preserving their differences.