Introducing Inheritance

 
Appendix A - Principles of Object-Oriented Programming
bySimon Robinsonet al.
Wrox Press 2002
  

One characteristic of objects in everyday life is that they tend to come in families of related things that share aspects of their design. My sofa is just like my armchairs, except that it can seat more than one person. A CD does the same sort of thing as a cassette tape, but with extra direct-access facilities.

Another example focuses upon cars. My car at the moment is a 13-year-old Ford Escort. Back in the 1980s, Ford had three big-selling cars out that all had a very similar design - the Escort, the Orion, and the Fiesta. The reason I'm thinking about these cars in particular is that they shared a lot more than just being Ford cars . They had different-shaped body shells , and the Fiesta was smaller, but internally their engines and other components were built in much the same way, often using the same components .

This is an example of implementation inheritance , and the equivalent in object-oriented programming would be some classes ( EscortCar , OrionCar, and FiestaCar , perhaps?), which not only expose methods with the same names , but actually the same methods, in the sense that when you call the methods you are running the same code.

Let's now extend this example. Say that I swapped my Escort for another Escort that has a diesel engine. Both cars have exactly the same body shell (the user interface is the same) but under the hood, the engines are different. That's an example of interface inheritance , and the equivalent in computer programming would be two classes, EscortCar and EscortDieselCar , which happen to expose methods that have the same names, purposes, and signatures, but different implementations .

Developers experienced in Java or in C++ and COM will recognize that implementation inheritance is the kind of inheritance that is supported by Java/C++ and other traditional object-oriented languages, while the more restricted interface inheritance was the only form of inheritance that was supported by COM and COM objects. VB supports only interface inheritance, through the Implements keyword. The great thing about C# is it supports both types of inheritance.

As far as C# programming is concerned , we're looking at the issue of how to define a new class, while reusing features from an existing class. The benefits are twofold - first, inheritance provides a convenient way to reuse existing, fully tested code in different contexts, thereby saving a lot of coding time, and second, inheritance can provide even more structure to your programs by giving a finer degree of granularity to your classes.

At this point we're going to move on to a new coding example, based on a cell phone company, which will demonstrate how implementation inheritance works in a C# program. Inheritance of classes in C# is always implementation inheritance. We'll leave interface inheritance for a while.

Using Inheritance in C#

The example we'll use to demonstrate inheritance is going to be of a fictitious cell phone company, which we'll call Mortimer Phones. We're going to develop a class that represents a customer account and is responsible for calculating that customer's phone bill. It's going to develop into a much longer, more complex sample than the Authenticator class, and as it develops we'll quickly find that one simple class is not adequate; rather, we are going to need a number of related classes, and in the next section inheritance will magically enter as the solution.

We're going to write a class that works out the monthly bill for each customer of Mortimer Phones. The class is called Customer , and each instance of this class represents one customer's account. In terms of public interface, the class will contain two properties:

  • Name representing the customer's name (read-write)

  • Balance , representing the amount owed (read-only)

There will also be two methods:

  • RecordPayment() , which is called to indicate that the customer has paid a certain amount of their bill.

  • RecordCall() , which is called when the customer has made a phone call. It works out the cost of the call and adds it to that customer's balance.

The RecordCall() method is potentially quite a complex function, since in the real world it would involve figuring out what the type of call was from the number called, then applying the appropriate tariff , and keeping a history of the calls. To keep things simple here, we'll assume there are just two types of calls: calls to landlines, and calls to other cell phones, and that each of these are charged at a flat rate of 2 cents a minute for landlines and 30 cents a minute for other cell phones. Our RecordCall method will simply be passed the type of call as a parameter, and we won't worry about keeping a call history.

With this simplification, we can look at the code for the project. The project was as usual created as a console application, and the first thing in it is an enumeration for the types of call:

 namespace Wrox.ProCSharp.OOProg {    using System;   public enum TypeOfCall     {     CallToCellPhone, CallToLandline     }   

Now, let's look at the definition of the Customer class:

   public class Customer     {     private string name;     private decimal balance;     public string Name     {     get     {     return name;     }     set     {     name = value;     }     }     public decimal Balance     {     get     {     return balance;     }     }     public void RecordPayment(decimal amountPaid)     {     balance -= amountPaid;     }     public void RecordCall(TypeOfCall callType, uint nMinutes)     {     switch (callType)     {     case TypeOfCall.CallToLandline:     balance += (0.02M * nMinutes);     break;     case TypeOfCall.CallToCellPhone:     balance += (0.30M * nMinutes);     break;     default:     break;     }     }     }   

This code should be reasonably self-explanatory. Note that we've hard-code the call charges of 2 cents/minute (land line) and 30 cents/minute (pay-as-you-go charges for a cell phone) into the program. In real life, they'd more likely to be read in from a relational database, or some file that allows the values to be changed easily.

Now let's add some code in the program's Main() method that displays the amounts of bills currently owing:

   public class MainEntryPoint     {     public static void Main()     {     Customer arabel = new Customer();     arabel.Name = "Arabel Jones";     Customer mrJones = new Customer();     mrJones.Name = "Ben Jones";     arabel.RecordCall(TypeOfCall.CallToLandline, 20);     arabel.RecordCall(TypeOfCall.CallToCellPhone, 5);     mrJones.RecordCall(TypeOfCall.CallToLandline, 10);     Console.WriteLine("{0,-20} owes ${1:F2}", arabel.Name, arabel.Balance);     Console.WriteLine("{0,-20} owes ${1:F2}", mrJones.Name, mrJones.Balance);     }     }     }   

Running this code gives the following results:

  MortimerPhones  Arabel Jones         owes .90 Ben Jones            owes 
  MortimerPhones  Arabel Jones owes $1.90 Ben Jones owes $0.20 
.20

Adding Inheritance

Currently, the Mortimer Phones example is heavily simplified. In particular, it only has one tariff for all customers, which is not remotely realistic. I'm registered under a tariff in which I pay a fixed rate each month, but there are loads of other schemes.

The way we're working at the moment, if we try to take all of the different tariffs into account, our RecordCall() method is going to end up containing various nested switch statements and looking something like this ( assuming the Tariff field is an enumeration):

   public void RecordCall(TypeOfCall callType, uint nMinutes)     {     switch (tariff)     case Tariff.Tariff1:     {     switch (callType)     {     case TypeOfCall.CallToLandline:     // work out amount     case TypeOfCall.CallToCellPhone:     // work out amount     // other cases     // etc.     }     case Tariff.Tariff2:     {     switch (callType)     {     // etc.     }   

That is not a satisfactory solution. Small switch statements are nice, but huge switch statements with large numbers of options - and in particular embedded switch statements - make for code that is difficult to follow. It also means that whenever a new tariff is introduced the code for the method will need to be changed. This could accidentally introduce new bugs into the parts of the code responsible for processing existing tariffs.

The problem is really to do with the way the code for the different tariffs is mixed up in a switch statement. If we could cleanly separate the code for the different tariffs the problem would be solved . This is one of the issues that inheritance addresses.

We want to separate out the code for different types of customers. We'll start by defining a new class, specifically represents customers on a new tariff, which we'll call the Nevermore60 tariff. Nevermore60 is designed for customers who use their cell phones a lot. Customers on this tariff pay a higher rate of 50 cents a minute for the first 60 minutes of calls to other cell phones, then a reduced rate of 20 cents a minute for all additional calls, so if they make a large enough number of calls they save money compared to the previous tariff.

We'll save actually implementing the new payment calculations for a little while longer, and we'll initially define Nevermore60Customer like this:

   public class Nevermore60Customer : Customer     {         }   

In other words, the class has no methods, no properties, nothing of its own. On the other hand, it's defined in a slightly different way from how we've defined any classes before. After the class name is a colon , followed by the name of our earlier class, Customer . This tells the compiler that Nevermore60Customer is derived from Customer . That means that every member in Customer also exists in Nevermore60Customer . Alternatively, to use the correct terminology, each member of Customer is inherited in Nevermore60Customer . Also, Nevermore60Customer is said to be a derived class , while Customer is said to be the base class . You'll also sometimes encounter derived classes referred to as subclasses , and base classes as superclasses or parent classes.

Since we've not yet put anything else in the Nevermore60Customer class, it is effectively an exact copy of the definition of the Customer class. We can create instances of and call methods against the Nevermore60Customer class, just as we could with Customer . To see this, we'll modify one of the customers, Arabel , to be a Nevermore60Customer :

 public static void Main() {   Nevermore60Customer arabel = new Nevermore60Customer();   ... } 

In this code, we've changed just one line, the declaration of Arabel , to make this customer a Nevermore60Customer instance. All the method calls remain the same, and this code will produce exactly the same results as our earlier code. If you want to try this out, it's the MortimerPhones2 code sample.

By itself, having a copy of the definition of the Customer class might not look very useful. The power of this comes from the fact we can now make some modifications or additions to Nevermore60Customer . We can effectively say to the compiler, " Nevermore60Customer is almost the same as Customer , but with these differences." In particular, we're going to modify the way that Nevermore60Customer works out the charge for each phone call according to the new tariff.

The differences we can specify in principle are:

  • We can add new members (of any type: fields, methods, properties, and so on) to the derived class, where these members are not defined in the base class

  • We can replace the implementation of existing members, such as methods or properties, that are already present in the base class

For our example, we will replace, or override , the RecordCall() method in Customer with a new implementation of the RecordCall() method in Nevermore60Customer . Not only that, but whenever we need to add a new tariff, we can simply create another new class derived from Customer , with a new override of RecordCall() . In this way we can add code to cope with many different tariffs, while keeping the new code separate from all the existing code that is responsible for calculations using existing tariffs.

Don't confuse method overriding with method overloading - the similarity in these names is unfortunate as they are completely different, unrelated, concepts. Method overloading has nothing to do with inheritance or virtual methods.

So let's modify the code for the Nevermore60Customer class, so that it implements the new tariff. To do this we need not only to override the RecordCall() method, but also to add a new field that indicates the number of high cost minutes that have been used:

 public class Nevermore60Customer : Customer    {   private uint highCostMinutesUsed;     public override void RecordCall(TypeOfCall callType, uint nMinutes)     {     switch (callType)     {     case TypeOfCall.CallToLandline:     balance += (0.02M * nMinutes);     break;     case TypeOfCall.CallToCellPhone:     uint highCostMinutes, lowCostMinutes;     uint highCostMinutesToGo =     (highCostMinutesUsed < 60) ? 60 - highCostMinutesUsed : 0;     if (nMinutes > highCostMinutesToGo)     {     highCostMinutes = highCostMinutesToGo;     lowCostMinutes = nMinutes - highCostMinutes;     }     else     {     highCostMinutes = nMinutes;     lowCostMinutes = 0;     }     highCostMinutesUsed += highCostMinutes;     balance += (0.50M * highCostMinutes + 0.20M *     lowCostMinutes);     break;     default:     break;     }     }     }   

You should note that the new field we've added, highCostMinutesUsed , is only stored in instances of Nevermore60Customer . It is not stored in instances of the base class, Customer . The base class itself is never implicitly modified in any way by the existence of the derived class. This must always be the case, because when you code up the base class, you don't necessarily know what other derived classes might be added in the future - and you wouldn't want your code to be broken when someone adds a derived class!

As you can see, the algorithm to compute the call cost in this case is rather more complex, though if you follow through the logic you will see it does meet our definition for the Nevermore60 tariff. Notice that the extra keyword override has been added to the definition of the RecordCall() method. This informs the compiler that this method is actually an override of a method that is already present in the base class, and we must include this keyword.

Before this code will compile, we need to make a couple of modifications to the base class, Customer , too:

 public class Customer    {       private string name;   protected decimal balance;   // etc.   public virtual void RecordCall(TypeOfCall callType, uint nMinutes)   {          switch (callType) 

The first change we've made is to the balance field. Previously it was defined with the private keyword, meaning that no code outside the Customer class could access it directly. Unfortunately this means that, even though Nevermore60Customer is derived from Customer , the code in the Nevermore60Customer class cannot directly access this field (even though a balance field is still present inside every Nevermore60Customer object). That would prevent Nevermore60Customer from being able to modify the balance when it records calls made, and so prevent the code we presented for the Nevermore60Customer.RecordCall() method from compiling.

The access modifier keyword protected solves this problem. It indicates that any class that is derived from Customer , as well as Customer itself, should be allowed access to this member. The member is still invisible, however, to code in any other class that is not derived from Customer . Essentially, we're assuming that, because of the close relationship between a class and its derived class, it's fine for the derived class to know a bit about the internal workings of the base class, at least as far as protected members are concerned.

There is actually a controversial point here about good programming style. Many developers would regard it as better practice to keep all fields private, and write a protected accessor method to allow derived classes to modify the balance. In this case, allowing the balance field to be protected rather than private prevents our example from becoming more complex than it already is.

The second change we've made is to the declaration of the RecordCall() method in the base class. We've added the keyword virtual . This changes the manner in which the method is called when the program is run, in a way that facilitates overriding it. C# will not allow derived classes to override a method unless that method has been declared as virtual in the base class. We will be looking at virtual methods and overriding later in this appendix.

Class Hierarchies and Class Design

In a procedural language, and even to some extent in a language like VB, the emphasis is very much on breaking the program down into functions. Object orientation shifts the emphasis of program design away from thinking about what functionality the program has to considering what objects the program consists of.

Inheritance is also an extremely important feature of object-oriented programming, and a crucial stage in the design of your program is deciding on class hierarchies - the relationships between your classes. In general, as with our Mortimer Phones example, you will find that you have a number of specialized objects that are all particular types of more generic objects.

When you're designing classes it's normally easiest to use a diagram known as a class hierarchy diagram , which illustrates the relationships between the various base and derived classes in your program. Traditionally, class hierarchy diagrams are drawn with the base class at the top and arrows pointing from derived classes to their immediate base classes. For example, the hierarchy of our Mortimer Phones examples from MortimerPhones3 onwards look like this:

click to expand

The above class hierarchy diagram emphasizes that inheritance can be direct or indirect. In our example, Nevermore60Customer is directly derived from Customer , but indirectly derived from Object . Although the examples in our discussion are tending to focus on direct derivation, all the principles apply equally when a class indirectly derives from another class.

Another example is one of the hierarchies from the .NET base classes. In Chapter 7, we see how to use the base classes that encapsulate windows (or to give them their more modern .NET terminology, forms). You may not have realized just how rich a hierarchy could be behind some of the controls that you can place on windows :

click to expand

The Form class represents the generic window, while ScrollBar , StatusBar , Button , TextBox , and RichTextBox represent the familiar corresponding controls. The rich hierarchy behind these classes allows a very finetuning of what implementations of which methods can be made common to a number of different classes. Many of these classes will also implement certain interfaces, by which they can make their nature as windows known to client code.

It's also important to realize that class hierarchies are, like any other aspect of programming, an area in which there may be many possible solutions, each of which having its own advantages and disadvantages. For our Mortimer Phones example, there may be other ways to design classes. One argument against our chosen hierarchy is that customers often change their tariffs - and do we really want to have to destroy a customer object and instantiate a new one of a different class whenever that happens?. Perhaps it would be better to have just one customer class, which contains a reference to a tariff object, and have a class hierarchy of tariffs?

A large application will not have just one hierarchy, but will typically implement a large number possibly stretching into hundreds of classes. That may sound daunting, but the alternative, before object-oriented programming came into being, was to have literally thousands of functions making up your program, with no way to group them into manageable units. Classes provide a very effective way of breaking your program into smaller sections. This not only helps maintenance, but also makes your program easier to understand because the classes represent the actual objects that your program is representing in a very intuitive way.

It's also important with your classes to think carefully about the separation between the public interface that is presented to client code, and the private internal implementation. In general the more of a class you are able to keep private, the more modular your program will become, in the sense that you can make modifications or improvements to the internal implementation of one class and be certain that it will not break or even have any effect on any other part of the program. That's the reason that we've emphasized that member fields in particular will almost invariably be private, unless they are either constant or they form part of a struct whose main purpose is to group together a small number of fields. We haven't always kept to that rule rigidly in this appendix, but that's largely so we can keep the samples as simple as possible.

The Object Class

One point that you might not realize from the code is that in our Mortimer Phones examples, Customer is itself derived from another class, the class Chapters 5.

Single and Multiple Inheritance

In C#, each derived class can only inherit from one base class (although we can create as many different classes that are derived from the same base class as we want). The terminology to describe this is single inheritance . Some other languages, including C++, allow you to write classes that have more than one base class, which is known as multiple inheritance .

Polymorphism and Virtual Members

Let's go back to our Mortimer Phones example. Earlier, we encountered this line of code:

 Nevermore60Customer Arabel = new Nevermore60Customer(); 

In fact, we could have instantiated the Nevermore60Customer object like this too:

   Customer Arabel = new Nevermore60Customer();   

Because Nevermore60Customer is derived from Customer , it's actually perfectly legitimate for a reference to a Customer to be set up to point to either a Customer or a Nevermore60Customer , or to an instance of any other class that is derived directly or indirectly from Customer . Notice that all we've changed here is the declaration of the reference variable. The actual object that gets instantiated with new is still a Nevermore60Customer object. If for example, you try to call GetType() against it, it'll tell you it's a Nevermore60Customer .

Being able to point to derived classes with a base reference may look like just a syntactical convenience, but it's actually essential if we want to be able use derived classes easily - and it's an essential feature of any language that aims to allow object-oriented programming. We can understand why if we think about how a real cell phone company will want to store the various Customer -derived classes. You see in our example we only had two customers, so it was easy to define separate variables , but more realistically we'd have hundreds of thousands of customers and we might want to do something like read them from a database into an array, then process them using the array, perhaps with code that looks like this:

   Customer[] customers = new Customer[NCustomers];     // do something to initialize customers     foreach (Customer nextCustomer in customers)     {     Console.WriteLine("{0,-20} owes ${1:F2}", nextCustomer.Name,     nextCustomer.Balance);     }   

If we use an array of Customer references, each element can point to any type of customer, no matter what Customer -derived class is used to represent that customer. However, if variables could not store references to derived types we'd have to have lots of arrays - an array of Customers , an array of Nevermore60Customers , and another array for each type of class.

Now that we've ensured that we can mix different types of class in one array, but this will give the compiler a new problem. Suppose we have a snippet of code like this:

   Customer aCustomer;     // Initialize aCustomer to a particular tariff     aCustomer.RecordCall(TypeOfCall.CallToLandline, 20);   

What the compiler can see is a Customer reference, and we are to call the RecordCall() method on it. The trouble is that aCustomer might refer to a Customer instance or it might refer to a Nevermore60Customer instance or it might refer to an instance of some other class derived from Customer . Each of these classes might have its own implementation of RecordCall() . How will the compiler determine which method should be called? There are two answers to this, depending on whether the method in the base class is declared as virtual and the derived class method as an override :

  • If the methods are not declared as virtual and override respectively then the compiler will simply use the type that the reference was declared to be. In this case, since aCustomer is of type Customer , it will arrange for the Customer.RecordCall() method to be called, no matter what aCustomer is actually referring to.

  • If the methods are declared as virtual and override respectively then the compiler will generate code that checks what the aCustomer reference is actually pointing to at run time. It then identifies which class this instance belongs to, and calls the appropriate RecordCall() override. This determination of which overload should be called will need to be made separately each time the statement is executed. For example, if the virtual method call occurs inside a foreach loop which executes 100 times, then on each iteration through the loop the reference might be pointing to a different instance and therefore to a different class of object.

In most cases, the second behavior is the one we want. If we have a reference, for example, to a Nevermore60Customer , then it's highly unlikely that we'd want to call any override of any method other than the one that applies to Nevermore60Customer instances. In fact, you might wonder why you'd ever want the compiler to use the first, non-virtual, approach, since it looks like that means in many cases the "wrong" override will be called up. Why we don't just make virtual methods the normal behavior, and say that every method is automatically virtual ? This is, incidentally, the approach taken by Java, which automatically makes all methods virtual . There are three good reasons, however, for not doing this in C#:

  • Performance .When a virtual method is called, a run-time determination needs to be made to identify which override needs to be called. For a non-virtual function, this information is available at compile-time. (The compiler can identify the relevant override from the type that the reference is declared as!) This means that for a non-virtual function, the compiler can perform optimizations such as inlining code to improve performance. Inlining virtual methods is not possible, which will hurt performance. Another, more minor, factor, is that the determination of the method itself gives a very small performance penalty. This penalty amounts to no more than an extra address lookup in a table of virtual function addresses (called a vtable), and so is insignificant in most cases, but may be important in very tight and frequently executed loops .

  • Design It may be the case that when you design a class there are some methods that should not be overridden. This actually happens a lot, especially with methods that should only be used internally within the class by other methods, or whose implementations reflect the internal class design. When you design a class, you choose which features of its implementation you make public, protected, or private. It's unlikely that you'll want methods that are primarily concerned with the internal operation of the class to be overrideable, so you typically won't declare these methods as virtual.

  • Versioning Virtual methods can cause a particular problem connected with releasing new versions of base classes. We examine these problems and the solutions in Chapter 3, when we discuss versioning.

The ability of a variable to be used to reference objects of different types, and to automatically call the appropriate version of the method of the object it references, is more formally known as polymorphism . However, you should note that in order to make use of polymorphism, the method you are calling must exist on the base class as well as the derived class. For example, suppose we add some other method, such as a property called HighCostMinutesLeft , to Nevermore60Customer in order to allow users to find out this piece of information. Then the following would be legal code:

   Nevermore60Customer mrLeggit = new Nevermore60Customer();     // processing     int minutesLeft = mrLeggit.HighCostMinutesLeft;   

The following, however, would not be legal code, because the HighCostMinutesLeft property doesn't exist in the Customer base class:

   Customer mrLeggit = new Nevermore60Customer();     // processing     int minutesLeft = mrLeggit.HighCostMinutesLeft;   

We also ought to mention some other points about virtual members:

  • It is not only methods that can be overridden or hidden. You can do the same thing with any other class member that has an implementation, including properties.

  • Fields cannot be declared as virtual or overridden. However, it is possible to hide a base version of a field by declaring another field of the same name in a derived class. In that case, if you wanted to access the base version from the derived class, you'd need to use the syntax base. < field_name >. Actually, you probably wouldn't do that anyway, because you'd have all your fields declared as private .

  • Static methods and so on cannot be declared as virtual , but they can be hidden in the same way that instance methods etc. can be. It wouldn't make sense to declare a static member as virtual; virtual means that the compiler looks up the instance of a class when it calls that member, but static members are not associated with any class instance.

  • Just because a method has been declared as virtual , that doesn't mean that it has to be overridden. In general, if the compiler encounters a call to a virtual method, it will look for the definition of the method first in the class concerned. If the method isn't defined or overridden in that class, it will call the base class version of the method. If the method isn't derived there, it'll look in the next base class, and so on, so that the method executed will be the one closest in the class hierarchy to the class concerned. (Note that this process occurs at compile time, when the compiler is constructing the vtable for each class. There is no impact at runtime.)

Method Hiding

Even if a method has not been declared as virtual in a base class, then it is still possible to provide another method with the same signature in a derived class. The signature of a method is the set of all information needed to describe how to call that method: its name, number of parameters, and parameter types. The new method will not, however, override the method in the base class. Rather, it is said to hide the base class method. As we've implied earlier, what this means is that the compiler will always examine the data type that the variable used to reference the instance is declared as when deciding which method to call. If a method hides a method in a base class, then you should normally add the keyword new to its definition. Not doing so does not constitute an error, but it will cause the compiler to give you a warning.

Realistically, method hiding is not something you'll often deliberately want to do, but we'll demonstrate how it would work by adding a new method called GetFunnyString() to our Customer class, and hiding it in Nevermore60Customer() . GetFunnyString() just displays some information about the class, and is defined like this:

 public class Customer    {   public string GetFunnyString()     {     return "Plain ordinary customer. Kaark!";     }   ...    public class Nevermore60Customer : Customer    {   public new string GetFunnyString()     {     return "Nevermore60. Nevermore!";     }   ... 

Nevermore60Customer 's version of this function will be the one called up, but only if called using a variable that is declared as a reference to Nevermore60Customer (or some other class derived from Nevermore60Customer ). We can demonstrate this with this client code:

 public static void Main()    {   Customer cust1;     Nevermore60Customer cust2;     cust1 = new Customer();     Console.WriteLine("Customer referencing Customer: "     + cust1.GetFunnyString());     cust1 = new Nevermore60Customer();     Console.WriteLine("Customer referencing Nevermore60Customer: "     + cust1.GetFunnyString());     cust2 = new Nevermore60Customer();     Console.WriteLine("Nevermore60Customer referencing: "     + cust2.GetFunnyString());     }   

This code is downloadable as the MortimerPhones3Funny sample. Running the sample gives this result:

  MortimerPhones3Funny  Customer referencing Customer: Plain ordinary customer. Kaark! Customer referencing Nevermore60Customer: Plain ordinary customer. Kaark! Nevermore60Customer referencing: Nevermore60. Nevermore! 

Abstract Functions and Base Classes

So far, every time we've defined a class we've actually created instances of that class, but that's not always the case. In many situations, you'll define a very generic class that you intend to derive other more specialized classes from, but don't ever intend to actually use. C# provides the keyword abstract for this purpose. If a class is declared as abstract it is not possible to instantiate it.

For example, suppose we have an abstract class MyBaseClass , declared like this:

   abstract class MyBaseClass     {     ...   

In this case the following statement will not compile:

   MyBaseClass MyBaseRef = new MyBaseClass();   

However, it's perfectly legitimate to have MyBaseClass references, so long as they only point to derived classes. For example, you can derive a new class from MyBaseClass :

   class MyDerivedClass : MyBaseClass     {     ...   

In this case, the following is all perfectly valid code:

   MyBaseClass myBaseRef;     myBaseRef = new MyDerivedClass();   

It's also possible to define a method as abstract . This means that the method is treated as a virtual method, and that you are not actually implementing the method in that class, on the assumption that it will be overridden in all derived classes. If you declare a method as abstract you do not need to supply a method body:

   abstract class MyBaseClass     {     public abstract int MyAbstractMethod();   // look no body!     ...   

If any method in a class is abstract , then that implies the class itself should be abstract , and the compiler will raise an error if the class is not so declared. Also, any non-abstract class that is derived from this class must override the abstract method. These rules prevent you from ever actually instantiating a class that doesn't have implementations of all its methods.

At this stage, you're probably wondering what the use of abstract methods and classes is. They are actually extremely useful for two reasons. One is that they often allow a better design of class hierarchy, in which the hierarchy more closely reflects the situation you are trying to model. The other is that the use of abstract classes can shift certain potential bugs from hard-to-locate run-time errors into easy-to-locate compile-time errors. It's a bit hard to see how that works in practice without looking at an example, so let's improve the program architecture of MortimerPhones by rearranging the class hierarchy.

Defining an Abstract Class

Before we start, let me say that we're not redesigning the Mortimer Phones sample just for the fun of it. There's actually a bit of a design flaw in the class hierarchy at the moment. Our class Customer represents pay-as-you-go customers as the base class for all the other customer types. We're treating that kind of tariff as if it's a special tariff from which all the others are derived. That's not really an accurate representation of the situation. In reality, the pay-as-you-go tariff is just one of a range of tariffs - there's nothing special about it - and a more carefully designed class hierarchy would reflect that. Therefore, in this section, we're going to rework the MortimerPhones sample to give it the class hierarchy shown in the diagram:

click to expand

Our old Customer class is gone. In its place is a new abstract base class, GenericCustomer . GenericCustomer implements all the stuff that is common to all types of customers, such as methods and properties that have the same implementation for all customers and so are not virtual. This will include such things as retrieving the balance or the customer's name or recording a payment.

GenericCustomer will not, however, provide any implementation of the RecordCall() method, which works out the cost of a given call and adds it to the customer's account. The implementation of this method is different for each tariff, so we require that every derived class supplies its own. Instead, GenericCustomer 's RecordCall() method will be declared as abstract .

Having done that, we need to add a class that represents the pay-as-you-go customers. The PayAsYouGoCustomer class does this job, supplying the override to RecordCall() that with our previous hierarchy was defined in the base Customer class.

You may wonder whether it is really worth the effort in redesigning the class hierarchy for the sample in this way. After all, the old hierarchy worked perfectly well didn't it? There is actually a practical reason for regarding the new hierarchy as a better designed architecture, in that it removes a possible subtle source of bugs.

In a real application, RecordCall() probably wouldn't be the only virtual method that needed to be implemented separately for each tariff. What happens if later on someone adds a new derived class, representing a new tariff, but forgets to add the overrides of some of these methods? Well, with the old class hierarchy, the compiler would have automatically substituted the corresponding method in the base class. With that hierarchy, the base class represented pay-as-you-go customers, so we would have ended up with subtle run-time bugs involving the wrong versions of methods being called. With our new hierarchy, however, that won't happen. Instead, we'll get a compile-time error, with the compiler complaining that the relevant abstract methods haven't been overridden in the new class.

Anyway, on to the new code, and as you may by now have guessed, this is the MortimerPhones4 sample. With the new hierarchy, the code for GenericCustomer looks like this. Most of the code is the same as for our old Customer class - in the following code we've highlighted the few lines that are different. Note the abstract declaration for the RecordCall() method:

   public abstract class GenericCustomer   {       ...       public void RecordPayment(decimal amountPaid)       {          balance -= amountPaid;       }   public abstract void RecordCall(TypeOfCall callType, uint nMinutes);   } 

Now for the implementation of pay-as-you-go customers. Again, notice that most of the code is taken directly from the former, obsolete, Customer , class. The only real difference is that RecordCall() is now an override rather than a virtual method:

   public class PayAsYouGoCustomer : GenericCustomer   {   public override void RecordCall(TypeOfCall callType, uint nMinutes)   {          // same implementation as for Customer       }    } 

We won't display the full code for Nevermore60Customer here as the RecordCall() override in this class is long and completely unchanged from the earlier version of the example. The only change we need to make to this class is to derive it from GenericCustomer instead of from the Customer class, which no longer exists:

   public class Nevermore60Customer : GenericCustomer   {       private uint highCostMinutesUsed;       public override void RecordCall(TypeOfCall callType, uint nMinutes)       {          // same implementation as for old Nevermore60Customer       }       ... 

To finish off, we'll add some new client code to demonstrate the operation of the new class hierarchy. This time we've actually used an array to store the various customers, so this code shows how an array of references to the abstract base class can be used to reference instances of the various derived class, with the appropriate overrides of the methods being called:

 public static void Main()       {   GenericCustomer arabel = new Nevermore60Customer();     arabel.Name = "Arabel Jones";     GenericCustomer mrJones = new PayAsYouGoCustomer();     mrJones.Name = "Ben Jones";     GenericCustomer [] customers = new GenericCustomer[2];     customers[0] = arabel;     customers[0].RecordCall(TypeOfCall.CallToLandline, 20);     customers[0].RecordCall(TypeOfCall.CallToCellPhone, 5);     customers[1] = mrJones;     customers[1].RecordCall(TypeOfCall.CallToLandline, 10);     foreach (GenericCustomer nextCustomer in customers)     {     Console.WriteLine("{0,-20} owes ${1:F2}", nextCustomer.Name,     nextCustomer.Balance);     }   } 

Running this code, once again, produces the correct results for the amounts owed:

  MortimerPhones4  Arabel Jones         owes .90 Ben Jones         owes 
  MortimerPhones4  Arabel Jones owes $2.90 Ben Jones owes $0.20 
.20

Sealed Classes and Methods

In many ways you can think of a sealed class or method as the opposite of an abstract class or method. Whereas declaring something as abstract means that it must be overridden or inherited from, declaring it as sealed means that it cannot be. Not all object-oriented languages support this concept, but it can be useful. In C# the syntax looks like this.

   sealed class FinalClass     {     ...   

C# also supports declaring an individual override method as sealed , preventing any further overrides of it.

The most likely situation when you'll mark a class or method as sealed will be if it is very much internal to the operation of the library, class, or other classes that you are writing, so you are fairly sure that any attempt to override some of its functionality will cause problems. You might also mark a class or method as sealed for commercial reasons, in order to prevent a third party from extending your classes in a manner that is contrary to the licensing agreements. In general, however, you should be careful about marking a class or member as sealed , since by doing so you are severely restricting how it can be used. Even if you don't think it would be useful to inherit from a class or override a particular member of it, it's still possible that at some point in the future someone will encounter a situation you hadn't anticipated in which it is useful to do so.

  


Professional C#. 2nd Edition
Performance Consulting: A Practical Guide for HR and Learning Professionals
ISBN: 1576754359
EAN: 2147483647
Year: 2002
Pages: 244

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net