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.

Table 1. Concrete class
Concrete class
Type-centricImplementation-centric
Defines a type and a set of operations on that typeProvides 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.

Table 2. Abstract class
Abstract class
Type-centricImplementation-centric
Defines a type and a set of operations on that typeOptionally 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.

Table 3. Interface
Interface
Type-centricImplementation-centric
Defines a type and a set of operations on that typeNo 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.

Figure 1. Plane and spherical geometry classes

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.

Figure 2. Common method implementations factored into an abstract class

The two geometry classes feature identical implementations for two methods:

  1. heading(Position), which determines the direction from each class's contained Position (class not shown) to a specified Position
  2. within(Position,double), which determines if the contained Position is within a circle defined by the specified center Position and double 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.

Figure 3. Common type operations factored into an interface

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.

Figure 4. Abstract class for implementation reuse and polymorphic behavior

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).

Figure 5. Add an elliptical geometry class

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.

Figure 6. Add a rouge geometry class

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.

Figure 7. Use an interface for type and an abstract class for implementation

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.

Figure 8. Extend the system with elliptical and rogue geometries

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.

Wm. Paul Rogers is a Java/object-oriented architect whose interests include teaching an increased understanding of Java design and implementation through stressing the first principles of fundamental object-oriented programming. He began using Java in the fall of 1995 in support of oceanographic studies conducted at the Monterey Bay Aquarium Research Institute, where he led the charge in utilizing new technologies to expand the possibilities of ocean science research. Paul has been using object-oriented methods for 10 years and works as an independent consultant in Bellingham, Wash.

Learn more about this topic

Join the discussion
Be the first to comment on this article. Our Commenting Policies