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.

1 2 Page
Recommended
Join the discussion
Be the first to comment on this article. Our Commenting Policies
See more