- Classes—Wrapping Data and Behavior Together
- Inheritance—Defining Classes in Terms of One Another
- Polymorphism—Overriding One Class Method with Another
- Summary
InheritanceDefining Classes in Terms of One Another
Inheritance is the capability for one class to inherit or take on the traits of another class. Typically, this happens in a hierarchical fashion. Consider the following simple example to see how this inheritance results in a natural hierarchy of classes. Figure 3.2 shows three classes: HR Employee, IT Employee, and Warehouse Employee. Each of these is shown with some of their properties and methods.
Figure 3.2 Three classesno parent class.
By looking at them, it quickly becomes clear that we could hierarchically structure these classes by abstracting their common traits into a parent class. These three classes would then be child classes of that one parent class. Figure 3.3 shows how this results in a tree structure for our classes. Another way to think about this is called sub-typing. Children classes can often be thought of as different "types" of the parent class (an HR employee is a type of Employee, and so on).
Figure 3.3 Introduction of a parent class.
This hierarchical structure is typical of well-engineered class librariesthe Framework Class Library is organized in just such a way.
Note
If you examine Figure 3.3, you will see that our arrows point from the child classes to the parent class. This may not seem intuitive to you; after all, aren't we creating a child class from a parent class? This notation is advocated because it shows that the child knows about the parent, but the parent does not (necessarily) know about the child.
Inheritance by Natural Relationship
One of the nice things about inheritance is that it often simply realizes a relationship that we already make in our minds. That is, it is often just a recognition of real-world relationships. We can tell that a dog or a cat is a type of an animalthe inheritance between the concept of an animal class and a dog or cat class is obvious. Again, this is a good thing as it helps to reduce the semantic gap that we talked about earlier and helps you make your code organization easier to understand. Organizing your code is only one benefit of inheritancecode reuse is another.
We have said that a class can inherit the traits of another class. We have also said that the traits of a class are implemented as property and method routines. When these routines are inherited between classes, it obviously means we are assuming the actual source code of one class into another. Thus, we have code reuse.
If we look back at Figure 3.3 we can see how each line of code that was written to implement the parent class methods can be reused by each of the child classes. Code reuse in this fashion becomes a powerful rapid application development enabler. If we needed to change some lines of code in one of the parent class methods, the change would be immediately realized in its children classes. This also allows us to build up complexity in a child class by inheriting from potentially simple base classes.
Note
There are many different ways to express the inheritance relationship: parent to child, super-class to sub-class, ancestor class to descendant class, generalized class to specialized class, and so on. In this book, anytime we use these terms you should know that we are just referring back to this basic concept of inheritance relationship.
Figure 3.4 shows class nomenclature, in ancestor/descendant terms, against a class tree.
Figure 3.4 Inheritance nomenclature examples.
We now know that identifying logical relationships between objects will help us out in the area of code reuse. But the examples we have talked about so far have been based on relationships between objectsan appraisal of one object being a type of another object. If, however, you approach inheritance by first looking at its end result, you'll find that you can end up with an entirely different perspective. Let's look at an example: Let's say that we have a class that defines operations for a specific type of printer. We'll call this class InkJet. Intuitively, we sense a parent class that would most likely be called Printer. Introducing a Printer parent class produces the inheritance that we see in Figure 3.5.
Inheriting from the Printer class is a good solution for us because it already defines some basic operations (line feed, paper out, and so on) that we can use as building blocks for our InkJet class. At the same time, we will add some of our own behaviors that are specific to inkjet printers. But what if we had the requirement for some low-level communication code that would send an error signal across a parallel port? Also, what if that code was already available to us in yet another class?
Figure 3.6 shows how we could inherit from a fictional ParallelPort object to leverage the SendErrorSignal code that we need.
Figure 3.5 Inheritance based on relationship.
Figure 3.6 Inheritance strictly for code reuse.
This is subtly different from what we were doing before because it is very difficult to envision a logical relationship between a parallel port object and an inkjet object. After all, an inkjet printer is not a type of a parallel portthere is no obvious hierarchical relationship to draw between the two. In this case, we would be implementing inheritance to get at raw code reuse. This doesn't do anything for us in terms of making our code easier to understandit does not reinforce a relationship between abstract classes and real-world objects.
Note
Inheriting for pure code reuse in the absence of a sub-type relationship is certainly something that you can do with classes, but isn't always the best approach. You gain code reuse at the expense of increased complexity in your system (and therefore, a corresponding increase in the effort required to understand your system). In .NET, we advocate implementing an interface instead of using class inheritance to represent this relationship; you still get the desired code reuse without complicating your class relationships (more on interfaces in Chapter 4).
What if you decided to proceed ahead with class inheritance, and decided to inherit from both the Printer and the ParallelPort class? This is called multiple inheritance (see Figure 3.7). Multiple inheritance is not supported by the .NET runtime.
Figure 3.7 An example of multiple.