Many of the inheritance examples so far have defined a class called PdaItem that defines the methods and properties common to Contact, Appointment, and so on, which are type objects that derive from PdaItem. PdaItem is not intended to be instantiated itself, however. A PdaItem instance has no meaning by itself; instead, it has meaning only when it is used as a base class—to share default method implementations across the set of data types that derive from it. These characteristics are indicative of the need for PdaItem to be an abstract class rather than a concrete class. Abstract classes are designed for derivation only. It is not possible to instantiate an abstract class, except in the context of instantiating a class that derives from it. Classes that are not abstract and can instead be instantiated directly are concrete classes.
Abstract classes are a fundamental object-oriented principle, so we describe them here accordingly. However, starting with C# 8.0, interfaces almost (specifically, no instance fields can be declared) support a superset of the functionality previously limited to abstract classes. While the details of the new interface capabilities are available in Chapter 8, understanding the concepts regarding abstract members is a prerequisite, so we will provide the details of abstract classes here.
Abstract classes represent abstract entities. Their abstract members define what an object derived from an abstract entity should contain, but they don’t include the implementation. Often, much of the functionality within an abstract class is unimplemented. Before a class can successfully derive from an abstract class, however, it needs to provide the implementation for the abstract methods in its abstract base class.
To define an abstract class, C# requires the abstract modifier to the class definition, as shown in Listing 7.16.
Although abstract classes cannot be instantiated, this restriction is a minor characteristic of an abstract class. Their primary significance is achieved when abstract classes include abstract members. An abstract member is a method or property that has no implementation. Its purpose is to force all derived classes to provide the implementation.
Consider Listing 7.17 as an example.
Listing 7.17 defines the GetSummary() member as abstract, so it doesn’t include any implementation. The code then overrides this member within Contact and provides the implementation. Because abstract members are supposed to be overridden, such members are automatically virtual and cannot be declared so explicitly. In addition, abstract members cannot be private because derived classes would not be able to see them.
It is surprisingly difficult to develop a well-designed object hierarchy. For this reason, when programming abstract types, you should be sure to implement at least one (and preferably more) concrete type that derives from the abstract type to validate the design.
If you don’t provide a GetSummary() implementation in Contact, the compiler will report an error.
C++ allows for the definition of abstract functions using the cryptic notation =0. These functions are called pure virtual functions in C++. In contrast to C#, however, C++ does not require the class itself to have any special declaration. Unlike C#’s abstract class modifier, C++ has no class declaration change when the class includes pure virtual functions.
When the implementation for the same member signature varies between two or more classes, the scenario demonstrates a key object-oriented principle: polymorphism. Poly means “many” and morph means “form,” so polymorphism refers to the existence of multiple implementations of the same signature. Also, because the same signature cannot be used multiple times within a single class, each implementation of the member signature occurs on a different class.
The idea behind polymorphism is that the object itself knows best how to perform a particular operation. Moreover, by enforcing common ways to invoke those operations, polymorphism encourages code reuse when taking advantage of the commonalities. Given multiple types of documents, each document type class knows best how to perform a Print() method for its corresponding document type. Therefore, instead of defining a single print method that includes a switch statement with the special logic to print each document type, with polymorphism you call the Print() method corresponding to the specific type of document you wish to print. For example, calling Print() on a word processing document class behaves according to word processing specifics, whereas calling the same method on a graphics document class will result in print behavior specific to the graphic. Given the document types, however, all you have to do to print a document is call Print(), regardless of the type.
Moving the custom print implementation out of a switch statement offers a number of maintenance advantages. First, the implementation appears in the context of each document type’s class rather than in a location far removed from it; this is in keeping with encapsulation. Second, adding a new document type doesn’t require a change to the switch statement. Instead, all that is necessary is for the new document type class to implement the Print() signature.
Abstract members are intended to be a way to enable polymorphism. The base class specifies the signature of the method, and the derived class provides the implementation (see Listing 7.18 with Output 7.5).
In this way, you can call the method on the base class, but the implementation is specific to the derived class. Output 7.5 shows that the List() method from Listing 7.18 is able to successfully display both Contacts and Addresses, and display them in a way tailored to each. The invocation of the abstract GetSummary() method actually invokes the overriding method specific to the instance.