Abstract classes, Interfaces, and Patterns |
A complete PDF version of the text book is now available. The PDF version is an almost complete subset of the HTML version (where only a few, long program listings have been removed). See here. |
This chapter is about abstract classes. At the end of the chapter we also touch on sealed classes. Relative to our interests, sealed classes are less important than abstract classes.
|
30.1. Abstract Classes
Contents Up Previous Next Slide Annotated slide Aggregated slides Subject index Program index Exercise index
When we program in the object-oriented paradigm it is important to work out concepts as general as possible. Programming at a general level promotes reusability (see Section 2.4).
In object-oriented programming languages we organize classes in hierarchies. The classes closest to the root are the most general classes. Take, as an example, the bank account class hierarchy in Section 25.3, where the class BankAccount is more general than CheckAccount, SavingsAccount, etc. It is worth noticing, however, that we were able to fully implement all operations in the most general class, BankAccount. In the rest of this chapter we will study even more general classes, for which we cannot (or will not) implement all the operations. The non-implemented operations are stated as declarations of intent at the most general level. These declarations of intent should be realized in less general subclasses.
Abstract classes are used for concepts that we cannot or will not implement in full details |
Here follows our definition of an abstract class and an abstract operation.
|
An abstract class
|
We will sometimes use the term concrete class for a class which is not abstract.
You should be aware that the definition of an abstract class, as given below, is not 100% accurate in relation to C#. In C# a class can be abstract without announcing abstract operations. More about that in Section 30.2 below, where we discuss abstract classes in C#.
The fact that an abstract class cannot be instantiated is the most tangible, operational consequence of working with abstract classes. Many OOP programmers tend to think of the abstract modifier as a mark, to be associated with those classes, he or she does not wish to instantiate. Surely, this is a consequence, but it is not the essential idea behind abstract classes.
30.2. Abstract classes and abstract methods in C#
Contents Up Previous Next Slide Annotated slide Aggregated slides Subject index Program index Exercise index
We will first study an example of an abstract class. We pick an abstract stack. (This is indeed a very popular example class in many contexts. We have tried to avoid it, but here it fits nicely).
The abstract class Stack, shown in Program 30.1, is remarkable of two reasons:
The blue parts of Program 30.1 are the abstract operations. These operations make up the classical stack operations Push, Pop, and Top together with Full, Empty, and Size. (Notice that Top, Full, Empty and Size are announced as properties, cf. Section 30.3). The abstract operations have signatures (method heads), but no body blocks. In a real-life version of the program we would certainly have supplied documentation comments with some additional explanations of the roles of the abstract operations in the class.
The purple part represents a fully implemented, "normal" method, called ToggleTop. This method swaps the order of the two top-most elements of the stack (if available). Notice that ToggleTop can be implemented solely in terms of the Push, Pop, Top and Size. In other words, it is not necessary for the implementation of ToggleTop to know details of the concrete data representation of stacks.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 | using System; public abstract class Stack{ abstract public void Push(Object el); abstract public void Pop(); abstract public Object Top{ get;} abstract public bool Full{ get;} abstract public bool Empty{ get;} abstract public int Size{ get;} public void ToggleTop(){ if (Size >= 2){ Object topEl1 = Top; Pop(); Object topEl2 = Top; Pop(); Push(topEl1); Push(topEl2); } } public override String ToString(){ return String.Format("Stack[{0}]", Size ); } } | |||
|
In Program 30.1 the method ToString is also an example of a fully implemented method, which relies on an abstract method, namely Size.
It is left as an exercise to implement a non-abstract subclass of the abstract stack, see Exercise 8.3.
Let us state some more detailed - a perhaps slightly surprising - observations about abstract classes and abstract operations. Each of them will be discussed below.
|
In relative rare situations an abstract class can inherit from a non-abstract class. Notice, however, that even abstract classes inherit (at least implicitly) from class Object, which is a non-abstract class in C#. (In principle, it would make good sense for the designers of C# to implement class Object as abstract class. But they did not! We only rarely make instances of class Object).
The next observation is about fully implemented classes, which we mark as being abstract. As discussed above, the purpose of this marking is to prevent instantiation of the class.
You may ask if it makes sense to have constructors in a class which never is instantiated. The answer is yes, because the data encapsulated in an abstract class A should be initialized when a concrete subclass of A is instantiated. Due to the rules of constructor cooperation, see Section 28.4 and Section 28.5, a constructor of class A will be activated. If no constructor is present in A, this falls back on the parameter-less default constructor.
Finally, we observe that the abstract methods are implicitly virtual. This is natural, because such a method has to be (re)defined in a subclass. In C# it is not allowed explicitly to write "virtual abstract" in front of an abstract method. Let us also observe, that an abstract method M cannot be private. This is because M need to be visible in the classes that override M.
Exercise 8.2. Course and Project classes In the earlier exercise about courses and projects (found in the lecture about classes) we programmed the classes BooleanCourse, GradedCourse, and Project. Revise and reorganize your solution (or the model solution) such that BooleanCourse and GradedCourse have a common abstract superclass called Course. Be sure to implement the method Passed as an abstract method in class Course. In the Main method (of the client class of Course and Project) you should demonstrate that both boolean courses and graded courses can be referred to by variables of static type Course. Solution |
Exercise 8.3. A specialization of Stack On the slide to which this exercise belongs, we have shown an abstract class Stack. It is noteworthy that the abstract Stack is programmed without any instance variables (that is, without any data representation of the stack). Notice also that we have been able to program a single non-abstract method ToggleTop, which uses the abstract methods Top, Pop, and Push. Make a non-abstract specialization of Stack, and decide on a reasonable data representation of the stack. In this exercise it is OK to ignore exception/error handling. You can, for instance, assume that the capacity of the stack is unlimited; That popping an empty stack an empty stack does nothing; And that the top of an empty stack returns the string "Not Possible". In a later lecture we will revisit this exercise in order to introduce exception handling. Exception handling is relevant when we work on full or empty stacks. Write a client of your stack class, and demonstrate the use of the inherited method ToggleTop. If you want, you can also adapt my stack client class . Solution |
30.3. Abstract Properties
Contents Up Previous Next Slide Annotated slide Aggregated slides Subject index Program index Exercise index
Properties were introduced in Chapter 18. Recall that properties allow us to get and set data of a class through getter and setter abstractions. From an application point of view, properties are used in the same way as variables - both on the left and right hand sides of assignments. Underneath, a property is realized as two methods - one "getter" and one "setter".
Properties can be abstract in the same way as methods. It means that we can announce a number of properties which must be fully defined in subclasses. We will in Program 30.2 study an example of abstract properties, namely in a Point class called AbstractPoint, which can be accessed both in a rectangular (x, y) and a polar (r, a) way. r and a means radius and angle respectively. There is no data (variables) in class AbstractPoint. We announce X, Y, R and A as abstract properties. These are emphasized using purple color. All of these are announced as both getters and setters. Notice the get; set; syntax. We could alternatively announce these as only getters, or as only setters. We notice that the syntax of abstract properties is similar to the syntax used for automatic properties, see Section 18.3.
Following the abstract properties comes three noteworthy methods Move, Rotate and ToString. They are shown in blue. They all use make heavy use the abstract properties. The assignment X += dx in Move, for instance, expands to X = X + dx. It first uses the getter of the X property on the right hand side of the assignment. Next, it uses the X setter on the left hand side. In Program 30.2 we only know that the X getter and the X setter exist. The actual implementation details will be found in a subclass.
In the last part of the class we see some static methods, which all are useful for the bread and butter calculations between polar and rectangular coordinates. They do not add to the client interface, because they are all protected. It is convenient to have them in class AbstractPoint, because they hereby will be available in all subclasses, which most certainly need them.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 | using System; abstract public class AbstractPoint { public enum PointRepresentation {Polar, Rectangular} // We have not yet decided on the data representation of Point public abstract double X { get ; set ; } public abstract double Y { get ; set ; } public abstract double R { get ; set ; } public abstract double A { get ; set ; } public void Move(double dx, double dy){ X += dx; Y += dy; } public void Rotate(double angle){ A += angle; } public override string ToString(){ return "(" + X + ", " + Y + ")" + " " + "[r:" + R + ", a:" + A + "] "; } protected static double RadiusGivenXy(double x, double y){ return Math.Sqrt(x * x + y * y); } protected static double AngleGivenXy(double x, double y){ return Math.Atan2(y,x); } protected static double XGivenRadiusAngle(double r, double a){ return r * Math.Cos(a); } protected static double YGivenRadiusAngle(double r, double a){ return r * Math.Sin(a); } } | |||
|
In Program 30.3 we see a subclass of AbstractPoint. It is called Point. It happens to represent points the polar way. But this is an internal (private) detail of class Point.
Class Point is a non-abstract class, and therefore we program a constructor, which is emphasized in black. The constructor is a little unconventional, because the first parameter allows us to specify if parameter two and three means x, y or radius, angle. It is desirable if this could be done more elegantly. (It can! Use of static factory methods, see Section 16.4, is better). Notice that PointRepresentation is an enumeration type defined in line 5 of Program 30.2.
Emphasized in purple we show the actual implementation of the X and Y properties. Let us look at X. The getter of X is called whenever X is used as a right-hand side value. It calculates the x-coordinate of a point from the radius and the angle. The setter of X is called when X is used in left-hand side context, such as X = e. The value of expression e is bound to the pseudo variable value. The setter calculates new radius and angle values which are assigned to the instance variables of class Point.
Emphasized in blue we show the implementation of the R and A properties. These are trivial compared to the X and Y properties, because we happen to represent points in the polar way.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 | using System; public class Point: AbstractPoint { // Polar representation of points: private double radius, angle; // radius, angle // Point constructor: public Point(PointRepresentation pr, double n1, double n2){ if (pr == PointRepresentation.Polar){ radius = n1; angle = n2; } else if (pr == PointRepresentation.Rectangular){ radius = RadiusGivenXy(n1, n2); angle = AngleGivenXy(n1, n2); } else { throw new Exception("Should not happen"); } } public override double X { get { return XGivenRadiusAngle(radius, angle);} set { double yBefore = YGivenRadiusAngle(radius, angle); angle = AngleGivenXy(value, yBefore); radius = RadiusGivenXy(value, yBefore); } } public override double Y { get { return YGivenRadiusAngle(radius, angle);} set { double xBefore = XGivenRadiusAngle(radius, angle); angle = AngleGivenXy(xBefore, value); radius = RadiusGivenXy(xBefore, value); } } public override double R { get { return radius;} set { radius = value;} } public override double A { get { return angle;} set { angle = value;} } } | |||
|
We are now ready to use class Point. We reuse most of the Point client program from Section 11.6. The program prompts the user for three points. The client program works mainly in rectangular coordinates, via the X and Y properties.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 | // A client of Point that instantiates three points and calculates // the circumference of the implied triangle. using System; public class Application{ public static Point PromptPoint(string prompt){ double x, y; AbstractPoint.PointRepresentation mode = AbstractPoint.PointRepresentation.Rectangular; Console.WriteLine(prompt); x = Double.Parse(Console.ReadLine()); y = Double.Parse(Console.ReadLine()); return new Point(mode,x,y); } public static void Main(){ AbstractPoint p1, p2, p3; double p1p2Dist, p2p3Dist, p3p1Dist, circumference; p1 = PromptPoint("Enter first point"); p2 = PromptPoint("Enter second point"); p3 = PromptPoint("Enter third point"); p1.Rotate(Math.PI); p2.Move(1.0, 2.0); p1p2Dist = Math.Sqrt((p1.X - p2.X) * (p1.X - p2.X) + (p1.Y - p2.Y) * (p1.Y - p2.Y)); p2p3Dist = Math.Sqrt((p2.X - p3.X) * (p2.X - p3.X) + (p2.Y - p3.Y) * (p2.Y - p3.Y)); p3p1Dist = Math.Sqrt((p3.X - p1.X) * (p3.X - p1.X) + (p3.Y - p1.Y) * (p3.Y - p1.Y)); circumference = p1p2Dist + p2p3Dist + p3p1Dist; Console.WriteLine("Circumference:\n {0}\n {1}\n {2}\n {3}", p1, p2, p3, circumference); } } | |||
|
Let us summarize what we have learned from the examples in Program 30.2, Program 30.3, and Program 30.4 (only on web). First and foremost, we have seen an abstract class in which we are able to implement useful functionality (Move, Rotate, and ToString) at a high level of abstraction. The implementation details in the mentioned methods rely on abstract properties, which are implemented in subclasses. We have also seen a sample subclass that implements the four abstract properties.
30.4. Sealed Classes and Sealed Methods
Contents Up Previous Next Slide Annotated slide Aggregated slides Subject index Program index Exercise index
We will now briefly, as the very last part of this chapter, describe sealed classes and sealed methods.
A sealed class C prevents the use of C as base class of other classes |
|
Sealed classes are related to static classes, see Section 11.12, in the sense that none of them can be subclassed. However, static classes are more restrictive because a static class cannot have instance members, a static class cannot be used as a type, and a static class cannot be instantiated. Sealed classes and methods correspond to final classes and final methods in Java.
In some sense, abstract and sealed classes represent opposite concepts. At least this holds in the following sense: A sealed class cannot be subclassed; An abstract class must be subclassed in order to be useful.
If a class is abstract it does not make sense that it is sealed. And the other way around, if a class is sealed it does not make sense that it, in addition, is abstract. Notice that it does not make sense either to have virtual methods in a sealed class.
A sealed class is not required to have sealed methods. Moreover, a class with a sealed method does not itself need to be sealed.
Finally, notice, that in C# a method cannot be sealed without also being overridden. Thus, the sealed modifier always occurs as an "extra modifier" of override. The intention of sealed methods is to prevent further overriding of virtual methods.