7.3. Abstract Classes

You learned in Chapter 5 how useful it can be to consolidate shared features—fields 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 their common fields 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 in which the need for generalization arose after the fact; now let's look at this problem from another perspective. Suppose that we have the foresight at the very outset of our project that we'll need various types of Course objects in our SRS: lecture courses, lab courses, independent study courses, and so on. We therefore want 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 methods:

  • EstablishCourseSchedule

  • EnrollStudent

  • AssignInstructor

We also need a few common fields:

  • string courseName;

  • string courseNumber;

  • int creditValue;

  • CollectionType enrolledStudents;

  • Professor instructor;

Some of these behaviors might 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
{
  private string courseName;
  private string courseNumber;
  private int creditValue;
  // Pseudocode.
  private Collection  enrolledStudents;
  private Professor instructor;

  // 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 behaviors (for example, EstablishCourseSchedule) might 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 might differ for different types of courses:

  • A lecture course may only meet once a week for 3 hours at a time.

  • A lab course may meet twice a week for 2 hours each time.

  • An independent study course may meet on a custom schedule that has been jointly negotiated by a given student and professor.

It would therefore seem to be a waste of time for us to bother trying to program a general-purpose 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 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 member instead? Part of our decision has to do with whether we ever plan to instantiate "general" Course objects in our application.

  • If we do, the Course class would need an EstablishCourseSchedule method of its own.

  • Even if we don't plan to instantiate the Course class directly, however, we still need to define an EstablishCourseSchedule method at the Course class level if we want to enable polymorphic behavior for this method.

Let's assume that we don't want to instantiate general Course objects, but do want 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?

OOPLs such as C# come to the rescue with the concept of abstract classes. An abstract class is used to specify 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 (for example, the EstablishCourseSchedule method in the 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:

// Note the use of the "abstract" keyword 
public abstract class Course
{
  private string courseName;
  private string courseNumber;
  private int creditValue;
  private List<Student> 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 (DateTime startDate,
                                                DateTime 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 (;)—that is, 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 instead we knew what we were doing when we intentionally omitted the body.

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

  • We specified a capability that objects of various Course types must be able to perform.

  • We detailed the means by which we'll ask such objects to perform this operation by defining a method header, which (as you learned in Chapter 4) controls the format of the method call that we'll pass to such objects when we want them to perform the operation.

  • Furthermore, we facilitated polymorphism—at least with respect to the method in question—by ensuring that all derived classes of Course will indeed recognize a method call involving this method signature.

However, we've done so without pinning down the private details of how the method will accomplish this task (that is, the business rules that apply for a given derived class). In essence, we 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, 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 (and almost always does) contain methods that have a body, known as concrete methods. Abstract classes also typically declare fields, as shown in the preceding Course class example. These fields could be available to all derived classes of the abstract class.

7.3.1.

7.3.1.1. 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, we can't ever instantiate Course objects in our application. This makes intuitive sense because if we could create an object of type Course, it would then be expected to know how to respond to a method call 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 method call.

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

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

c.EstablishCourseSchedule(dateTimeStart, dateTimeEnd);  // Behavior undefined!

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

error: 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.

7.3.1.2. Overriding Abstract Methods

When we derive a class from an abstract base class, the derived class will inherit all the base class's members, including all 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 (DateTime startDate,
                                                DateTime 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(DateTime startDate,
 DateTime 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 provided a concrete method body.

Unless a derived class provides a concrete implementation for all 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 its ancestors' abstract methods if it wants to "break the spell of abstractness" (that is, if we want to instantiate objects of that derived type—see Figure 7-2).

Figure 7.2. "Breaking the spell" of abstractness by overriding abstract methods

7.3.1.3. 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 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
{
  // Other class details omitted.

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

public class LabCourse : Course
{
  // Other class details ommitted.

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

//  This class won't compile. See details below...
							public class IndependentStudyCourse : Course
{
  // Other class details omitted.

  // 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:  'IndependentStudyCourse' does not implement inherited
abstract member 'Course.EstablishCourseSchedule(DateTime, DateTime)'

Unless we go back and amend the IndependentStudyCourse class declaration to reflect it as being abstract:

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

We just hit upon how abstract methods serve to enforce implementation requirements! Declaring an abstract method in a base class ultimately constrains 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.

In addition to abstract methods, properties can also be declared to be abstract. This feature can be useful if you want all derived classes to provide their own implementation of computing a property value (a student's GPA, for instance). When an abstract property is declared, only the get and set keywords are included in the declaration without any bodies to the get and set accessors.

public abstract string Gpa { get; set; }

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset