Making The Most Of Your Inheritance
- Inheriting from a Class
- Polymorphism and Type Substitution
- Replacing Methods in a Derived Class
- Summary
Many programmers have long considered inheritance to be one of the most significant design features of OOP. Inheritance was made popular more than two decades ago by languages such as C++ and Smalltalk. Since then, new languages (e.g., Java) have come along and refined the features and syntax for using inheritance. Now with the emergence of the .NET Framework, Microsoft has designed a platform from the ground up that offers support for what is arguably one of the most elegant forms of inheritance to date.
The more you use the .NET Framework, the more you will realize just how extensively it takes advantage of inheritance. For example, as discussed in Chapter 3, the CTS relies heavily on inheritance. When you use the Windows Forms package, your new forms will inherit from an existing form-based class in the FCL. When you use ASP.NET, your Web pages and Web services will inherit from preexisting classes in the FCL. As a developer building applications or component libraries based on the .NET Framework, you will find that familiarity with inheritance is an absolute necessity.
Inheriting from a Class
Inheritance allows you to derive a new class from an existing class. Suppose that class C inherits from class B. Then class B is typically called the base class, and class C is called the derived class. Note that this terminology is not necessarily standard; some refer to B and C as superclass and subclass, or as parent and child classes, respectively. This book will stick with the terms “base” and “derived.”
A derived class definition automatically inherits the implementation and the programming contract of its base class. The idea is that the derived class starts with a reusable class definition and modifies it by adding more members or by changing the behavior of existing methods and properties.
One of the primary reasons for designing with inheritance is that it provides an effective means for reusing code. For example, you can define a set of fields, methods, and properties in one class, and then use inheritance to reuse this code across several other classes. Inheritance is particularly beneficial in scenarios that require multiple classes with much in common but in which the classes are all specialized in slightly different ways. Examples include the following familiar categories: employees (administrative, technical, sales), database tables (customers, orders, products), and graphical shapes (circle, rectangle, triangle).
Listing 5.1 A simple base class
'*** base class Public Class Human '*** private implementation Private m_Name As String '*** public members Public Property Name() As String '*** provide controlled access to m_Name End Property Public Function Speak() As String Return "Hi, I'm a human named " & m_Name End Function End Class
Let's start by looking at a simple example to introduce the basic syntax of inheritance. Listing 5.1 defines a class named Human that we would like to use as a base class. In Visual Basic .NET, when you want to state explicitly that one class inherits from another, you follow the name of the class with the Inherits keyword and the name of the base class. For example, here are two derived class definitions for Manager and Programmer, both with Human as their base class:
'*** first derived class Public Class Manager Inherits Human '*** code to extend Human definition End Class '*** second derived class Public Class Programmer : Inherits Human '*** code to extend Human definition End Class
For the Programmer class, the preceding code fragment uses a colon instead of an actual line break between the class name and the Inherits keyword. As you might recall from earlier versions of Visual Basic, the colon acts as the line termination character. This colon-style syntax is often preferred because it improves readability by keeping the name of a derived class on the same line as the name of its base class.
The Human class serves as a base class for both the Manager class and the Programmer class. It's now possible to write implementations for these classes that are quite different as well as to add new members to each class to further increase their specialization. Nevertheless, these two classes will always share a common set of implementation details, and both support a unified programming contract defined by the Human class.
By default, every class you create in Visual Basic .NET can be inherited by other classes. If for some reason you don't want other programmers to inherit from your class, you can create a sealed class. A sealed class is defined in the Visual Basic .NET language using the keyword NotInheritable. The compiler will generate a compile-time error whenever a programmer attempts to define a class that inherits from such a sealed class:
Public NotInheritable Class Monkey '*** sealed class definition '*** implementation End Class Public Class Programmer : Inherits Monkey '*** compile-time error! End Class
Figure 5.1 shows a common design view of class definitions known as an inheritance hierarchy. The Human class is located at the top of the hierarchy. The Manager class and the Programmer class inherit from the Human class and are consequently located directly below it in the inheritance hierarchy. Notice the direction of the arrows when denoting inheritance.
Figure 5.1. An inheritance hierarchy
As shown in Figure 5.1, an inheritance hierarchy can be designed with multiple levels. Consider the two classes at the bottom of the hierarchy in Figure 5.1, SeniorProgrammer and JuniorProgrammer. These classes have been defined with the Programmer class as their base class. As a consequence, they inherit indirectly from the Human class. Thus a class in a multilevel inheritance hierarchy inherits from every class reachable by following the inheritance arrows upward.
An important design rule should always be applied when designing with inheritance. Two classes that will be related through inheritance should be able to pass the is-a test. In short, the test goes like this: If it makes sense to say “C is a B,” then it makes sense for class C to inherit from class B. If such a sentence doesn't make sense, then you should reconsider the use of inheritance in this situation.
For example, you can correctly say that a programmer “is a” human. You can also correctly say that a senior programmer “is a” programmer. As you can see, the purpose of the is-a test is to ensure that a derived class is designed to model a more specialized version of whatever entity the base class is modeling.
You should try never to establish an inheritance relationship between two classes that cannot pass the is-a test. Imagine you saw a novice programmer trying to make the Bicycle class inherit from the Wheel class. You should intervene because you cannot correctly say that a bicycle “is a” wheel. You can correctly say that a bicycle “has a” wheel (or two), but that relationship calls for a different design technique that doesn't involve inheritance. When you determine that two entities exhibit the “has a” relationship, the situation most likely calls for a design using containment, in which the Wheel class is used to define fields within the Bicycle class. In other words, the Bicycle class contains the Wheel class.
Base Classes in the .NET Framework
Now that you've seen some of the basic principles and the syntax for using inheritance, it's time to introduce a few important rules that have been imposed by the .NET Common Type System (CTS).
The first rule is that you cannot define a class that inherits directly from more than one base class. In other words, the CTS does not support multiple inheritance. It is interesting to note that the lack of support for multiple inheritance in the .NET Framework is consistent with the Java programming language.
The second rule imposed by the CTS is that you cannot define a class that doesn't have a base class. This rule might seem somewhat confusing at first, because you can write a valid class definition in Visual Basic .NET (or in C#) that doesn't explicitly declare a base class. A little more explanation is required to clarify this point.
When you define a class without explicitly specifying a base class, the compiler automatically modifies the class definition to inherit from the Object class in the FCL. Once you understand this point, you can see that the following two class definitions have the same base class:
'*** implicitly inherit from Object Public Class Dog End Class '*** explicitly inherit from Object Public Class Cat : Inherits System.Object End Class
These two CTS-imposed rules of inheritance can be summarized as follows: Every class (with the exception of the Object class) has exactly one base class. Of course, it's not just classes that have base types. For example, every structure and every enumeration also has a base type. Recall from Chapter 3 that the inheritance hierarchy of the CTS is singly rooted because the system-defined Object class serves as the ultimate base type (see Figure 3.1). Every other class either inherits directly from the Object class or inherits from another class that inherits (either directly or indirectly) from the Object class.
Inheriting Base Class Members
Although a derived class inherits the members of its base class, the manner in which certain kinds of members are inherited isn't completely intuitive. While the way things work with fields, methods, and properties is fairly straightforward, the manner in which constructors are inherited brings up issues that are a bit more complex.
Inheritance and Fields, Methods, and Properties
Every field, method, and property that is part of the base class definition is inherited by the derived class definition. As a result, each object created from a derived type carries with it all the states and behaviors that are defined by its base class. However, whether code in a derived class has access to the members inherited from its base class is a different matter altogether.
As mentioned in Chapter 4, each member of a class is defined with a level of accessibility that determines whether other code may access it. There are five levels of accessibility:
-
Private. A base class member defined with the Private access modifier is not accessible to either code inside the derived class or client-side code using either class.
-
Protected. A base class member defined with the Protected access modifier is accessible to code inside the derived class but is not accessible to client-side code. Private and protected members of a base class are similar in that they are not accessible to client-side code written against either the base class or the derived class.
-
Public. A base class member defined with the Public access modifier is accessible to code inside the derived class as well as all client-side code. Public members are unlike private and protected members in that they add functionality to the programming contract that a derived class exposes to its clients.
-
Friend. A member that is defined with the Friend access modifier is accessible to all code inside the containing assembly but inaccessible to code in other assemblies. The friend level of accessibility is not affected by whether the accessing code is part of a derived class.
-
Protected Friend. The protected friend level of accessibility is achieved by combining the Protected access modifier with the Friend access modifier. A protected friend is accessible to all code inside the containing assembly and to code within derived classes (whether the derived class is part of the same assembly or not).
Listing 5.2 presents an example that summarizes the discussion of what is legal and what is not legal with respect to accessing fields of varying levels of accessibility.
Listing 5.2 The meaning of access modifiers in the presence of inheritance
'*** base class Public Class BettysBaseClass Private Field1 As Integer Protected Field2 As Integer Public Field3 As Integer End Class '*** derived class Public Class DannysDerivedClass : Inherits BettysBaseClass Public Sub Method1() Me.Field1 = 10 '*** illegal (compile-time error) Me.Field2 = 20 '*** legal Me.Field3 = 30 '*** legal End Sub End Class '*** client-side code Module BobsApp Public Sub Main() Dim obj As New DannysDerivedClass() obj.Field1 = 10 '*** illegal (compile-time error) obj.Field2 = 20 '*** illegal (compile-time error) obj.Field3 = 30 '*** legal End Sub End Module
Now that we have outlined the rules of member accessibility, it's time to discuss how to properly encapsulate base class members when designing for inheritance. Encapsulation is the practice of hiding the implementation details of classes and assemblies from other code. For example, a protected member is encapsulated from client-side code. A private member is encapsulated from client-side code and derived classes. A friend member is encapsulated from code in other assemblies.
Imagine you are designing a component library that you plan to sell to other companies. You will update this component library from time to time and send the newest version to your customers. If your design involves distributing base classes that other programmers will likely extend through the use of inheritance, you need to think very carefully through the issues of defining various base class members as private versus protected.
Any member defined as private is fully encapsulated and can be modified or removed without violating the original contract between a base class and any of its derived classes. In contrast, members defined as protected are a significant part of the contract between a base class and its derived classes. Modifying or removing protected members can introduce breaking changes to your customer's code.
To keep your customers happy, you must devise a way to maintain and evolve the base classes in your component library without introducing breaking changes. A decade's worth of experience with inheritance has told the software industry that this challenge can be very hard to meet.
When authoring base classes, it's critical to start thinking about versioning in the initial design phase. You must determine how easy (or how difficult) it will be to modify derived classes if modifications to base classes cause breaking changes. It helps to design with the knowledge that it's a common mistake to underestimate the importance of encapsulating base class members from derived classes.
Another important consideration is whether it makes sense to use inheritance across assembly boundaries. While the compilers and the plumbing of the CLR are more than capable of fusing a base class implementation from one assembly together with the derived class implementation in a second assembly, you should recognize that versioning management grows ever more difficult as the scope of the inheritance increases.
That doesn't mean that you should never use cross-assembly inheritance. Many experienced designers have employed this strategy very effectively. For example, when you leverage one of the popular .NET frameworks such as Windows Forms or ASP.NET, you're required to create a class in a user-defined assembly that inherits from a class in a system-defined assembly. But understand one thing: The designers at Microsoft who created these frameworks thought long and hard about how to maintain and evolve their base classes without introducing breaking changes to your code.
If you plan to create base classes for use by programmers in other development teams, you must be prepared to think through these same issues. It is naive to ponder encapsulation only in terms of stand-alone classes, and only in terms of a single version. Inheritance and component-based development make these issues much more complex. They also make mistakes far more costly. In general, you shouldn't expose base class members to derived classes and/or other assemblies if these members might ever change in name, type, or signature. Following this simple rule will help you maintain backward compatibility with existing derived classes while evolving the implementation of your base classes.
Inheritance and Constructors
The way in which constructors are inherited isn't as obvious as for other kinds of base class members. From the perspective of a client attempting to create an object from the derived class, the derived class definition does not inherit any of the constructors defined in the base class. Instead, the derived class must contain one or more of its own constructors to support object instantiation. Furthermore, each constructor defined in a derived class must call one of the constructors in its base class before performing any of its own initialization work.
Recall from Chapter 4 that the Visual Basic .NET compiler will automatically create a public default constructor for any class definition that does not define a constructor of its own. This default constructor also contains code to call the default constructor of the base class. As an example, consider the following class definition:
'*** a class you write Public Class Dog End Class
Once compiled, the definition of this class really looks like this:
'*** code generated by compiler Public Class Dog : Inherits System.Object '*** default constructor generated by compiler Public Sub New() MyBase.New() '*** call to default constructor in System.Object End Sub End Class
As this example reveals, the compiler will generate the required constructor automatically along with a call to the base class's default constructor. But what about the situation in which the base class does not have an accessible default constructor? In such a case, the derived class definition will not compile because the automatically generated default constructor is invalid. As a result, the author of the derived class must provide a constructor of his or her own, with an explicit call to a constructor in the base class.
The only time a derived class author can get away with not explicitly defining a constructor is when the base class provides an accessible default constructor. As it turns out, the Object class contains a public default constructor, which explains why you don't have to explicitly add a constructor to a class that inherits from the Object class. Likewise, you don't have to explicitly add a constructor to a class that inherits from another class with an accessible default constructor.
Sometimes, however, you must inherit from a class that doesn't contain a default constructor. Consider the following code, which includes a revised definition of the Human class discussed earlier. In particular, note that the Human class now contains a parameterized constructor:
Public Class Human Protected m_Name As String Public Sub New(ByVal Name As String) m_Name = Name End Sub Public Function Speak() As String Return "Hi, I'm a human named " & m_Name End Function End Class Public Class Programmer : Inherits Human '*** this class definition will not compile End Class
Because this definition contains a single parameterized constructor, the compiler doesn't automatically add a default constructor to class Human. As a result, when you try to define the Programmer class without an explicit constructor, your code will not compile because Visual Basic .NET cannot generate a valid default constructor. To make the Programmer class compile, you must add a constructor that explicitly calls an accessible constructor defined in the Human class:
Public Class Programmer : Inherits Human Public Sub New(Name As String) MyBase.New(Name) '*** call to base class constructor '*** programmer-specific initialization goes here End Sub End Class
As shown in the preceding code, you make an explicit call to a base class constructor by using MyBase.New and passing the appropriate list of parameters. Note that when you explicitly call a base class constructor from a derived class constructor, you can do it only once and the call must be the first statement in the constructor's implementation.
Of course, you never have to rely on compiler-generated calls to the default constructor. In some cases you might prefer to call a different constructor. An explicit call to MyBase.New can always be used at the top of a derived class constructor to call the exact base class constructor you want. Some programmers even add explicit calls to the default constructor by using MyBase.New. Even though such calls can be automatically generated by the compiler, making these calls explicit can serve to make your code self-documenting and easier to understand.
Let's take a moment and consider the sequence in which the constructors are executed during object instantiation by examining the scenario where a client creates an object from the Programmer class using the New operator. When the client calls New, a constructor in the Programmer class begins to execute. Before this constructor can do anything interesting, however, it must call a constructor in the Human class. The constructor in the Human class faces the same constraints. Before it can do anything interesting, it must call the default constructor of the Object class.
The important observation is that constructors execute in a chain starting with the least-derived class (i.e., Object) and ending with the most-derived class. The implementation for the constructor of the Object class always runs to completion first. In the case of creating a new Programmer object, when the constructor for the Object class completes, it returns and the constructor for the Human class next runs to completion. Once the constructor for the Human class returns, the constructor for the Programmer class runs to completion. After the entire chain of constructors finishes executing, control finally returns to the client that started the sequence by calling the New operator on the Programmer class.
Limiting Inheritance to the Containing Assembly
Now that you understand the basics of how constructors must be coordinated between a base class and its derived classes, let's explore a useful design technique that prevents other programmers in other assemblies from inheriting from your base classes. The benefit of using this technique is that you can take advantage of inheritance within your assembly, but prevent cross-assembly inheritance. This approach eliminates the need to worry about how changes to the protected members in the base class might introduce breaking changes to code in other assemblies. Why would you want to do this? An example will describe a situation in which you might find this technique useful.
Suppose you're designing a component library in which you plan to use inheritance. You've already created an assembly with a base class named Human and a few other classes that derive from Human, such as Manager and Programmer. In this scenario, you will benefit from the features of inheritance inside the scope of your assembly. In an effort to minimize your versioning concerns, however, you want to limit the use of inheritance to your assembly alone. To be concise, you'd like to prevent classes in other assemblies from inheriting from your classes.
Of course, it's a simple matter to prevent other programmers from inheriting from the derived classes such as Programmer and Manager: You simply declare them as sealed classes using the NotInheritable keyword. Such a declaration makes it impossible for another programmer to inherit from these classes. However, you cannot define the Human class using the NotInheritable keyword because your design relies on it serving as a base class. Also, you cannot define a base class such as Human as a friend class when public classes such as Programmer and Manager must inherit from it. The CTS doesn't allow a public class to inherit from a friend class, because in general the accessibility of an entity must be equal to or greater than the accessibility of the new entity being defined (a new entity can restrict access, but cannot expand access).
To summarize the problem, you want the Human class to be inheritable from within the assembly and, at the same time, to be non-inheritable to classes outside the assembly. There is a popular design technique that experienced software designers often use to solve this problem—they create a public base class with constructors that are only accessible from within the current assembly. You accomplish this by declaring the base class constructors with the Friend keyword:
'*** only inheritable from within the current assembly! Public Class Human Friend Sub New() '*** implementation End Sub End Class
Now the definition of the Human class is inheritable from within its assembly but not inheritable to classes in other assemblies. While this technique is not overly intuitive at first, it can prove very valuable. It allows the compiler to enforce your design decision so as to prohibit cross-assembly inheritance. Again, the reason to use this technique is to help simplify a design that involves inheritance. Remember that cross-assembly inheritance brings up many issues that are often best avoided when they're not a requirement.