Team LiB
Previous Section Next Section

Abstract Classes

We learned in Chapter 5 how useful it can be to consolidate shared features— attributes and behaviors—of two or more classes into a common base class, a process known as generalization. We did this when we created the Person class as a generalization of Student and Professor and then moved the declarations of all of their common attributes, methods, and properties into this base class. By doing so, the Student- and Professor-derived classes both became simpler, and we eliminated a lot of redundancy that would otherwise have made maintenance of the SRS much more cumbersome.

The preceding example involved a situation where the need for generalization arose after the fact; now let's look at this problem from the opposite perspective. Say that we have the foresight at the very outset of our project that we're going to need various types of Course objects in our SRS: lecture courses, lab courses, independent study courses, etc. We therefore wish to start out on the right foot by designing a Course base class to be as versatile as possible to facilitate future specialization.

We might determine up front that all Courses, regardless of type, are going to need to share a few common attributes:

as well as a few common behaviors:

Some of these behaviors may be generic enough so that we can afford to program them in detail for the Course class, knowing that it's a pretty safe bet that any future derived classes of Course will inherit these methods "as is" without needing to override them; for example:

public class Course
{
  string courseName;
  string courseNumber;
  int creditValue;
  Professor instructor;
  // Pseudocode.
  Collection enrolledStudents;

  // Properties provided; details omitted ...

  public bool EnrollStudent(Student s) {
    // Pseudocode.
    if (we haven't exceeded the maximum allowed enrollment yet)
      enrolledStudents.Add(s);
}
  public void AssignInstructor(Professor p) {
    Instructor = p;
  }
}

However, other of the behaviors may be too specialized for a given derived type to enable us to come up with a useful generic version. For example, the business rules governing how to schedule class meetings may differ for different types of courses:

It would therefore seem to be a waste of time for us to bother trying to program a generic version of the EstablishCourseSchedule method for the Course class, because one size simply can't fit all in this situation; all three types would have to override such logic to make it meaningful for them.

Can we afford to just omit the EstablishCourseSchedule method from the Course class entirely, adding such a method to each of the derived classes of Course as a new feature instead? Part of our decision has to do with whether or not we ever plan on instantiating "generic" Course objects in our application.

Let's assume that we don't want to instantiate generic Course objects, but do wish to take advantage of polymorphism. We're faced with a dilemma! We know that we'll need a type-specific EstablishCourseSchedule method to be programmed for all derived classes of Course, but we don't want to go to the trouble of programming code in the parent class that will never serve a useful purpose. How do we communicate the requirement for such a behavior in all derived classes of Course and, more importantly, enforce its future implementation?

OO languages such as C# come to the rescue with the concept of abstract classes. An abstract class is used to enumerate the required behaviors of a class without having to provide an explicit implementation of each and every such behavior. We program an abstract class in much the same way that we program a nonabstract class (also known informally as a concrete class), with one exception: for those behaviors for which we can't (or care not to) devise a generic implementation—e.g., the EstablishCourseSchedule method in our preceding example—we're permitted to specify method headers without having to program the corresponding method bodies. We refer to a "bodiless," or header-only, method specification as an abstract method.

Let's go back to our Course class definition to add an abstract method as highlighted in the following code:

public abstract class Course
{
  private string courseName;
  private string courseNumber;
  private int creditValue;
  private ArrayList enrolledStudents;
  private Professor instructor;

  // Other details omitted.

  public bool EnrollStudent(Student s) {
    // Pseudocode.
    if (we haven't exceeded the maximum allowed enrollment yet) {
      enrolledStudents.Add(s);
    }
  }

  public void AssignInstructor(Professor p) {
    Instructor = p;
  }

  // Note the use of the "abstract" keyword and the terminating semicolon.
  public abstract void EstablishCourseSchedule (string startDate,
                                                string endDate);
}

The EstablishCourseSchedule method is declared to be abstract by adding the abstract keyword to its header. Note that the header of an abstract method has no braces following the closing parenthesis of the parameter list. Instead, the header is followed by a semicolon (;)—i.e., it's missing its code body, which normally contains the detailed logic of how the method is to be performed. The method must therefore be explicitly labeled as abstract to notify the compiler that we didn't accidentally forget to program this method, but rather that we knew what we were doing when we intentionally omitted the body.

By specifying an abstract method, we've accomplished several very important goals:

However, we've done so without pinning down the private details of how the method will accomplish this task—i.e., the business rules that apply for a given derived class. We've in essence specified what a Course type object needs to be able to do without constraining how it must be done. This gives each derived type of CourseLectureCourse, LabCourse, IndependentStudyCourse—the freedom to define the inner workings of the method to reflect the business rules specific to that particular derived type by overriding the abstract method with a concrete version.

Whenever a class contains one or more abstract methods, then the class as a whole must be designated to be an abstract class through inclusion of the abstract keyword in the class declaration:

public abstract class Course
{
  // details omitted
}

Note that it isn't necessary for all methods in an abstract class to be abstract; an abstract class can also contain methods that have a body, known as concrete methods.

Abstract Classes and Instantiation

There is one caveat with respect to abstract classes: they can't be instantiated. That is, if we define Course to be an abstract class in the SRS, then we can't ever instantiate generic Course objects in our application. This makes intuitive sense, for if we could create an object of type Course, it would then be expected to know how to respond to a message to establish a course schedule, because the Course class declares a method header for the EstablishCourseSchedule behavior. But because there is no code behind that method, the Course object in question wouldn't know how to behave in response to such a message.

The compiler comes to our assistance by preventing us from even writing code to instantiate an abstract class in the first place; if we were to try to compile the following code snippet, for example:

Course c = new Course();  // Impossible! The compiler will generate an error
                          // on this line of code.
// details omitted ...

c.EstablishCourseSchedule('01/10/2001', '05/15/2001');  // Behavior undefined!

we'd get the following compilation error on the first line of code:

error CS0144: cannot create an instance of the abstract class or
interface 'Course'

While we're indeed prevented from instantiating an abstract class, we're nonetheless permitted to declare reference variables to be of an abstract type:

Course x;  // This is OK.

Why would we ever want to declare reference variables of type Course if we can't instantiate objects of type Course? The answer has to do with facilitating polymorphism; you'll learn the importance of being able to define reference variables of an abstract type when we talk about iterating through generic C# collections in more depth in Chapter 13.

Overriding Abstract Methods

When we derive a class from an abstract base class, the derived class will inherit all of the base class's features, including all of its abstract method headers. The derived class may replace an inherited abstract method with a concrete version using the override keyword, as illustrated in the following code:

// The abstract base class.
public abstract class Course
{
  private string courseName;
  // etc.

  // Other details omitted.

  public abstract void EstablishCourseSchedule (string startDate,
                                                 string endDate);
}
  // Deriving a class from an abstract base class.
  public class LectureCourse : Course
  {
    // Details omitted.

    // Replace the abstract method with a concrete method.
    public override void EstablishCourseSchedule(string startDate,
                                                 string endDate) {
    // Logic specific to the business rules for a LectureCourse ...
    // details omitted.
  }
}

We used the override keyword in similar fashion in Chapter 5, when it was used to override a virtual method declared in a base class. Note that in the preceding example, we've dropped the abstract keyword off of the overridden EstablishCourseSchedule method in the LectureCourse derived class, because the method is no longer abstract; we've provided a concrete method body.

Unless a derived class provides a concrete implementation for all of the abstract methods that it inherits from an abstract base class, the derived class will automatically be rendered abstract, as well. In such a situation, the derived class of course can't be instantiated, either. Therefore, somewhere in the derivation hierarchy, a class derived from an abstract class must have concrete implementations for all of its ancestors' abstract methods if it wishes to "break the spell of abstractness"— i.e., if we wish to instantiate objects of that derived type (see Figure 7-3).

Click To expand
Figure 7-3: "Breaking the spell" of abstractness by overriding abstract methods

"Breaking the Spell" of Abstractness

Let's look at a detailed example. Having intentionally designed Course as an abstract class earlier to serve as a common template for all of the various course types we envision needing for the SRS, we later decide to derive classes LectureCourse, LabCourse, and IndependentStudyCourse. In the following code snippet, we show these three derived classes of Course; of these, only two—LectureCourse and LabCourse— provide implementations for the abstract EstablishCourseSchedule method, and so the third derived class—IndependentStudyCourse—remains abstract and can't be instantiated.

public class LectureCourse : Course
{
  // All attributes are inherited from the Course class; no new
  // attributes are added.

  public override void EstablishCourseSchedule (string startDate,
                                                string endDate) {
    // Logic would be provided here for how a lecture course
    // establishes a course schedule; details omitted ...
  }
}

public class LabCourse : Course
{
  // All attributes are inherited from the Course class; no new
  // attributes are added.

  public override void EstablishCourseSchedule (string startDate,
                                                string endDate) {
    // Logic would be provided here for how a lab course establishes a
    // course schedule; details omitted ...
  }
}

public class IndependentStudyCourse : Course
{
  // All attributes are inherited from the Course class; no new
  // attributes are added.

  // We are purposely choosing NOT to implement the
  // EstablishCourseSchedule method in this derived class.
}

If we were to try to compile the preceding code, the C# compiler would force us to flag the IndependentStudyCourse class with the abstract keyword; that is, we'd get the following compilation error:

error CS0534: 'IndependentStudyCourse' does not implement inherited
abstract member 'Course.EstablishCourseSchedule(string, string)'

unless we go back and amend the IndependentStudyCourse class declaration to declare it as abstract:

public abstract class IndependentStudyCourse : Course
{
  // details omitted ...
}

We've just hit upon how abstract methods serve to enforce implementation requirements! Declaring an abstract method in a base class ultimately forces all derived classes to provide type-specific implementations of all inherited abstract methods; otherwise, the derived classes themselves can't be instantiated.

Note that having allowed IndependentStudyCourse to remain an abstract class isn't necessarily a mistake; the only error was subsequently trying to instantiate it. We may plan on deriving another "generation" of classes from IndependentStudyCourse—perhaps IndependentStudyGraduateCourse and IndependentStudyUndergraduateCourse—making them concrete in lieu of making IndependentStudyCourse concrete. It's perfectly acceptable to have multiple layers of abstract classes in an inheritance hierarchy; we simply need a terminal/leaf class to be concrete in order for it to be useful in creating objects.

We've seen that a derived class of an abstract class can be made concrete by providing nonabstract implementations of all abstract methods declared by the abstract class. It's also possible to go the other direction as well: that is, a derived class can override a nonabstract method declared in a base class with an abstract method. We'll explore examples of this when we discuss the Object class in Chapter 13.


Team LiB
Previous Section Next Section