Maximize flexibility with interfaces and abstract classes
Design with interfaces and abstract classes to satisfy both type and implementation issues
Though type is an extremely important object-oriented concept, it is often overlooked in favor of implementation-centric concerns. Java program development is as much about design as about implementation. I don’t see much advantage in totally separating the two. Good developers simultaneously consider design and implementation issues at all times during development, and program design decisions often revolve around the system’s type structure. Ignoring or de-emphasizing a system’s type-centric views tends to confuse that system’s design.
Introductory discussions of the difference between interfaces and abstract classes exemplify the implementation-centric view adopted by most Java texts. Such discussions explain how to use either an interface or an abstract class, but seldom explain why you would choose to use one or the other. Through a simple example, this article investigates the design decisions driving the use of interfaces and abstract classes and shows why interfaces satisfy type concerns and abstract classes satisfy implementation concerns.
Groundwork
To lay the groundwork necessary for a clear discussion, I review Java class and interface constructs from both type and implementation-centric viewpoints. As detailed in my earlier article, “Thanks Type and Gentle Class” (JavaWorld, January 2001), classes and interfaces establish a system’s type hierarchy, whereas only classes establish the implementation hierarchy. Classes divide into two types: concrete and abstract.
I’ll briefly review three constructs: concrete classes, abstract classes, and interfaces. I start with the concrete class.
Concrete class
In Java, because concrete classes do not require a special designation during declaration, they are typically just called classes. Each declared class performs double duty. From a type perspective, the class defines a type and a set of operations for the class’s object instances. From an implementation perspective, the class provides implementation code for each declared operation. These implementation modules are the class methods.
Classes inherit operations from supertypes and methods from superclasses. The restriction that each class extend only one direct superclass means the implementation hierarchy follows a single inheritance policy. Since a class can implement one or more interfaces and extend one other class, type operations can be inherited from multiple supertypes. Thus, Java supports multiple inheritance in the type hierarchy.
Concrete class | |
---|---|
Type-centric | Implementation-centric |
Defines a type and a set of operations on that type | Provides implementation for each declared type operation |
Abstract class
The abstract
keyword in the class declaration identifies an abstract class. From a type perspective, the type-defining characteristics of an abstract class match those of a concrete class. The two differ in that an abstract class optionally provides implementation code for each declared operation. An abstract class can define an abstract method (that is, define a type operation without method implementation) by using the abstract
keyword in the method declaration.
An abstract class does not need any abstract methods; however, any class with an abstract method must be declared abstract. An important characteristic of abstract classes is that they cannot be used to instantiate objects. A runtime object must be capable of responding to any legal method call, the legality of which is determined at compile-time and governed by the system’s declared type structure. An object instantiated from an abstract class could receive a valid method call, but have no implementation code for the specified operation. Therefore, all objects must instantiate from concrete classes to guarantee the availability of runtime implementation code for each permissible object method call.
Abstract class | |
---|---|
Type-centric | Implementation-centric |
Defines a type and a set of operations on that type | Optionally provides implementation for declared type operations |
Interface
In Java, an interface defines a type and a set of operations on that type, just like a class. From a type perspective, an interface’s type-defining characteristics match those of concrete and abstract classes. Interfaces, however, do not permit any method implementations, so from an implementation perspective, interfaces guarantee no implementation. That is key to enabling multiple type inheritance while guaranteeing single implementation inheritance.
Interface | |
---|---|
Type-centric | Implementation-centric |
Defines a type and a set of operations on that type | No implementation |
As indicated in the above three tables, from a type perspective, concrete classes, abstract classes, and interfaces are identical constructs. Each defines a type and the set of permissible operations on that type. From an implementation perspective, the three constructs provide a full range of method implementation possibilities. Concrete classes guarantee that all type operations have method implementations, abstract classes permit type operations with optional method implementations, and interfaces guarantee type operations with no method implementations.
Since the three constructs have identical type-defining characteristics, you might surmise that type doesn’t play a role in choosing between them. However, don’t forget about the all-important matter of inheritance. Restricted to single implementation inheritance, the class construct cannot combine types defined by multiple classes in distinct implementation hierarchies, which significantly influences designing systems for polymorphic behavior.
Implementation reuse
To bring pragmatism to the previous section’s borderline theoretic discussion, consider the development of classes representing possible geometries for dealing with positions defined by the Global Positioning System (GPS). Depending on specific problem domain needs, you can use various geometries for such tasks as calculating distance and direction between GPS positions. Two commonly used geometries are plane geometry, which models the earth’s surface using local Cartesian coordinates, and spherical geometry, which models the earth’s surface as a sphere. Figure 1 shows a simple UML class diagram for these two geometries.
The detailed algorithms used in these classes stretch beyond this article’s scope. Our discussion pertains to the implementation and type hierarchy design decisions that best utilize the classes. Given this common scenario of two or more specializations of a single generality, object-oriented design experience suggests looking for ways to structure the system. The question is, based on what criteria?
Developers are typically taught to first look for ways to reuse implementation code. Given this criteria, common implementations in the PlaneGeometry
and SphericalGeometry
classes factor into a superclass. Applying this technique yields the class diagram in Figure 2.
The two geometry classes feature identical implementations for two methods:
heading(Position)
, which determines the direction from each class’s containedPosition
(class not shown) to a specifiedPosition
within(Position,double)
, which determines if the containedPosition
is within a circle defined by the specified centerPosition
anddouble
radius
These two methods, in turn, utilize geometry-specific implementations of the methods course(Position)
and radians(Position)
, which are, therefore, declared as abstract methods. A rudimentary outline of class Geometry
looks like the following:
//
// Geometry.java
//
package gps;
public abstract class Geometry
{
abstract public double radians( Position destination );
abstract public double course( Position destination );
public boolean within( Position center, double radians )
{
// Implementation calls radians(Position).
}
public double heading( Position destination )
{
// Implementation calls course(Position).
}
protected Position position;
}
Note that although PlaneGeometry
and SphericalGeometry
each declare a within(Position,Position)
method to determine if the contained Position
is within a box defined by the specified Position
objects, this operation is not factored into the abstract class Geometry
. Certainly it could be, and later will be, but doing so now rejects the current structuring criteria of achieving implementation code reuse. Because no common implementation is available, the operation is not included in the abstract class Geometry
. From an implementation-centric viewpoint, the only abstract methods necessary in Geometry
are those used by concrete methods within that class.
Polymorphic behavior
The previous section took an implementation-centric approach to creating structure for the two specialized geometry classes. Another approach follows the lines of type-oriented thinking. One of the most powerful object-oriented techniques is subtype polymorphism, the ability to handle multiple specialized types from a single, supertype viewpoint. (See “Reveal the Magic Behind Subtype Polymorphism,” Wm. Paul Rogers, (JavaWorld, April 2001), for more details about subtype polymorphism.) From a type-centric viewpoint, enabling polymorphic behavior becomes a criterion for adding structure to the two geometry classes. Given this criteria, common operations in the PlaneGeometry
and SphericalGeometery
types factor into a supertype. Applying this technique yields the class diagram in Figure 3.
Each geometry class defines five common type operations that factor into the interface Geometry
. With this type structure, objects instantiated from classes PlaneGeometry
and SphericalGeometry
can be uniformly handled through a Geometry
-typed reference variable. During program execution, a Geometry
reference variable can polymorphically attach to various PlaneGeometery
and SphericalGeometry
objects, and method calls to the underlying objects execute the appropriate class implementation code.
Have your cake and eat it too
The solution depicted in Figure 3 does not address implementation reuse concerns. As pointed out in the discussion leading to Figure 2, the methods heading(Position)
and within(Position,double)
in classes PlaneGeometry
and SphericalGeometry
are identical, meaning the solution in Figure 3 duplicates the code for those methods in each concrete class. Surely there must be a way to simultaneously achieve implementation reuse and enable polymorphic behavior. There are, in fact, two ways to do just that. As a first attempt, adding the operation within(Position,Position)
to Figure 2’s abstract class yields the same set of type operations as the interface used in Figure 3. Using an abstract class to combine implementation reuse and enable polymophic behavior in this manner yields the class diagram depicted in Figure 4.
We seemingly have our cake and get to eat it too. From an implementation perspective, the abstract class Geometry
becomes a repository for the duplicate implementations of the methods heading(Position)
and within(Position,double)
. From a type perspective, Geometry
defines all the common type operations of the two specialized geometry classes.
What could be better? We merrily go about our business and later receive a request to extend our design. The local Cartesian coordinates used in the PlaneGeometry
class work adequately for small distances, and the spherical coordinates used in the SphericalGeometry
class are great for a sphere. However, the earth is not a sphere. Because the radius at the equator is somewhat greater than at the poles, an ellipse more accurately describes the earth. The International Reference Ellipsoid serves as the earth’s standard elliptical model. To use this geometry, the class EllipticalGeometry
is added to the design as depicted in Figure 5 (operations are suppressed in the diagram).
That offers a valid solution, provided EllipticalGeometry
shares the common code factored out of PlaneGeometry
and SphericalGeometry
. Suppose another request asks for the addition of a RogueGeometry
class. Furthermore, suppose we discover that although this rogue geometry adheres to the same set of Geometry
type operations, the implementation code for the methods heading(Position)
and within(Position,double)
is not valid. Figure 6 depicts our plight.
The RogueGeometry
class is not easily associated with the abstract class Geometry
. How can you facilitate polymorphic behavior for class RogueGeometry
? You could extend Geometry
as the other classes do and override the method implementations contained in Geometry
, but that defeats the purpose of having an abstract class act as a common code repository. Furthermore, suppose RogueGeometry
needs to pull implementation from a class in another class hierarchy. Java’s single implementation inheritance restriction prevents adding such a class to the current class structure.
Really have your cake and eat it too
The problem depicted in Figure 6 stems from using an abstract class to define a type for enabling subtype polymorphism. Recall that the original goal was both to achieve implementation reuse and to enable polymorphic behavior for the two concrete geometry classes PlaneGeometry
and SphericalGeometry
. An abstract class works well as a common implementation repository, but not so well as a supertype. A more flexible solution limits an abstract class’s use to implementation reuse and employs an interface for enabling subtype polymorphism. Figure 7 depicts this solution.
The interface Geometry
defines the type necessary to handle the concrete classes polymorphically. The abstract class AbstractGeometry
serves as an implementation repository for the common code found in the two concrete classes. Using each construct to satisfy a specific need meets the original goal. Interfaces facilitate type-centric concerns, and abstract classes satisfy the implementation-centric concerns. Best of all, the design proves quite flexible. Figure 8 depicts a solution for extending the design by adding the classes EllipticalGeometry
and RogueGeometry
.
EllipticalGeometry
extends AbstractGeometry
to utilize the common implementation in that class, whereas RogueGeometry
implements the Geometry
interface directly, thereby bypassing the code repository in AbstractGeometry
. Bypassing AbstractGeometry
‘s implementation also frees RogueGeometry
to extend a more appropriate base class if necessary. The type defined by the Geometry
interface serves as the necessary glue to polymorphically handle objects instantiated from any of the four concrete geometry classes. The ease in extending the original design directly relates to having properly used interfaces and abstract classes to handle type and implementation-centric concerns.
Skeletal implementations
The class/type structure in Figure 7 results from a bottom-up approach in class design. Factoring out common type operations in the bottom concrete classes leads to a top-most interface, and factoring out common implementation code leads to an intermediate abstract class. A similar structure also results from a top-down approach that starts first with the top-most interface. This approach is particularly useful when the top-level interface’s type is initially determined, but full implementation details for concrete classes is not known.
That situation can arise when modeling an abstraction, such as a List
, without knowing or perhaps without wishing to commit to the full implementation details necessary for concrete type realizations. However, some implementation details might be available, and you can add that implementation to an abstract class extending the interface-defined type. The resulting abstract class is called a skeletal implementation. Skeletal implementations can provide valuable implementation assistance to developers who eventually create concrete classes that extend the skeletal abstract class. The Java libraries contain several examples of skeletal implementations, such as java.util.AbstractList
, javax.swing.border.AbstractBorder
, and javax.swing.text.AbstractDocument
.
Regardless of whether the class/type structure results from bottom-up or top-down design, the fundamental issue remains: the design draws strength from properly using an interface to define the type needed for polymorphic behavior and an abstract class to provide an implementation repository for concrete subclasses.
Maximize flexibility and extendibility
This article’s discussion points to the following two general guidelines:
- Use interfaces for defining types
- Use abstract classes for common implementation repositories
Although interfaces and abstract classes each define a type and the set of operations on that type, use the type-defining characteristics of abstract classes with caution. Interfaces provide much more design flexibility for defining types. As part of the implementation inheritance hierarchy, abstract classes are restricted to a single inheritance policy. Interfaces allow the definition of types whose implementations might actually span multiple class hierarchies.
Fortunately, the above guidelines do not limit design possibilities. Coupling a type-defining interface with the partial implementation of an abstract class leads to a flexible and extendible class/type structure. Concrete classes are free to choose whether the common implementation in the abstract class is appropriate or whether to directly extend the interface and possibly extend some other class. Either way, all the concrete classes can be treated polymorphically through the interface-defined type.