Designing with Interfaces

advertisement
Designing with Interfaces
One Programmer's Struggle to Understand the
Interface
by Bill Venners
Summary
In this installment of my Design Techniques
column, I describe the process I went through to
understand Java's interface. I talk about multiple
inheritance and the diamond problem,
polymorphism and dynamic binding, separation of
interface and implementation as the spirit of Java,
and my ultimate epiphany on how we should think
about and use interfaces when we design. (3,500
words)
One of the fundamental activities of any software system design
is defining the interfaces between the components of the system.
Because Java's interface construct allows you to define an
abstract interface without specifying any implementation, a major
activity of any Java program design is "figuring out what the
interfaces are." This article looks at the motivation behind the
Java interface and gives guidelines on how to make the most of
this important part of Java.
Deciphering the interface
Almost two years ago, I wrote a chapter on the Java interface
and asked a few friends who know C++ to review it. In this
chapter, which is now part of my Java course reader Objects and
Java (see Resources), I presented interfaces primarily as a
special kind of multiple inheritance: multiple inheritance of
interface (the object-oriented concept) without multiple
inheritance of implementation. One reviewer told me that,
although she understood the mechanics of the Java interface
after reading my chapter, she didn't really "get the point" of them.
Exactly how, she asked me, were Java's interfaces an
improvement over C++'s multiple inheritance mechanism? At the
time I wasn't able to answer her question to her satisfaction,
primarily because in those days I hadn't quite gotten the point of
interfaces myself.
Although I had to work with Java for quite a while before I felt I
was able to explain the significance of the interface, I noticed one
difference right away between Java's interface and C++'s
multiple inheritance. Prior to the advent of Java, I spent five
years programming in C++, and in all that time I had never once
used multiple inheritance. Multiple inheritance wasn't against my
religion exactly, I just never encountered a C++ design situation
where I felt it made sense. When I started working with Java,
what first jumped out at me about interfaces was how often they
were useful to me. In contrast to multiple inheritance in C++,
which in five years I never used, I was using Java's interfaces all
the time.
So given how often I found interfaces useful when I began
working with Java, I knew something was going on. But what,
exactly? Could Java's interface be solving an inherent problem in
traditional multiple inheritance? Was multiple inheritance of
interface somehow intrinsically better than plain, old multiple
inheritance?
Interfaces and the 'diamond problem'
One justification of interfaces that I had heard early on was that
they solved the "diamond problem" of traditional multiple
inheritance. The diamond problem is an ambiguity that can occur
when a class multiply inherits from two classes that both
descend from a common superclass. For example, in Michael
Crichton's novel Jurassic Park, scientists combine dinosaur DNA
with DNA from modern frogs to get an animal that resembled a
dinosaur but in some ways acted like a frog. At the end of the
novel, the heros of the story stumble on dinosaur eggs. The
dinosaurs, which were all created female to prevent fraternization
in the wild, were reproducing. Chrichton attributed this miracle of
love to the snippets of frog DNA the scientists had used to fill in
missing pieces of the dinosaur DNA. In frog populations
dominated by one sex, Chrichton says, some frogs of the
dominant sex may spontaneously change their sex. (Although
this seems like a good thing for the survival of the frog species, it
must be terribly confusing for the individual frogs involved.) The
dinosaurs in Jurassic Park had inadvertently inherited this
spontaneous sex-change behavior from their frog ancestry, with
tragic consequences.
This Jurassic Park scenario potentially could be represented by
the following inheritance hierarchy:
Figure 1. Multiple inheritance in Jurassic Park
The diamond problem can arise in inheritance hierarchies like
the one shown in Figure 1. In fact, the diamond problem gets its
name from the diamond shape of such an inheritance hierarchy.
One way the diamond problem can arise in the Jurassic Park
hierarchy is if both Dinosaur and Frog, but not Frogosaur,
override a method declared in Animal. Here's what the code
might look like if Java supported traditional multiple inheritance:
abstract class Animal {
abstract void talk();
}
class Frog extends Animal {
void talk() {
System.out.println("Ribit, ribit.");
}
class Dinosaur extends Animal {
void talk() {
System.out.println("Oh I'm a dinosaur and I'm
OK...");
}
}
// (This won't compile, of course, because Java
// only supports single inheritance.)
class Frogosaur extends Frog, Dinosaur {
}
The diamond problem rears its ugly head when someone tries to
invoke talk() on a Frogosaur object from an Animal reference,
as in:
Animal animal = new Frogosaur();
animal.talk();
Because of the ambiguity caused by the diamond problem, it isn't
clear whether the runtime system should invoke Frog's or
Dinosaur's implementation of talk(). Will a Frogosaur croak
"Ribbit, Ribbit." or sing "Oh, I'm a dinosaur and I'm
okay..."?
The diamond problem would also arise if Animal had declared a
public instance variable, which Frogosaur would then have
inherited from both Dinosaur and Frog. When referring to this
variable in a Frogosaur object, which copy of the variable -Frog's or Dinosaur's -- would be selected? Or, perhaps, would
there be only one copy of the variable in a Frogosaur object?
In Java, interfaces solve all these ambiguities caused by the
diamond problem. Through interfaces, Java allows multiple
inheritance of interface but not of implementation.
Implementation, which includes instance variables and method
implementations, is always singly inherited. As a result,
confusion will never arise in Java over which inherited instance
variable or method implementation to use.
Interfaces and polymorphism
In my quest to understand the interface, the diamond problem
explanation made some sense to me, but it didn't really satisfy
me. Sure, the interface represented Java's way of dealing with
the diamond problem, but was that the key insight into the
interface? And how did this explanation help me understand how
to use interfaces in my programs and designs?
As time went by I began to believe that the key insight into the
interface was not so much about multiple inheritance as it was
about polymorphism (see the explanation of this term below).
The interface lets you take greater advantage of polymorphism in
your designs, which in turn helps you make your software more
flexible.
Ultimately, I decided that the "point" of the interface was:
Java's interface gives you more polymorphism than
you can get with singly inherited families of classes,
without the "burden" of multiple inheritance of
implementation.
A refresher on polymorphism
This section will present a quick refresher on the meaning of
polymorphism. If you are already comfortable with this fancy
word, feel free to skip to the next section, "Getting more
polymorphism."
Polymorphism means using a superclass variable to refer to a
subclass object. For example, consider this simple inheritance
hierarchy and code:
abstract class Animal {
abstract void talk();
}
class Dog extends Animal {
void talk() {
System.out.println("Woof!");
}
}
class Cat extends Animal {
void talk() {
System.out.println("Meow.");
}
}
Given this inheritance hierarchy, polymorphism allows you to
hold a reference to a Dog object in a variable of type Animal, as
in:
Animal animal = new Dog();
The word polymorphism is based on Greek roots that mean
"many shapes." Here, a class has many forms: that of the class
and any of its subclasses. An Animal, for example, can look like
a Dog or a Cat or any other subclass of Animal.
Polymorphism in Java is made possible by dynamic binding, the
mechanism by which the Java virtual machine (JVM) selects a
method implementation to invoke based on the method
descriptor (the method's name and the number and types of its
arguments) and the class of the object upon which the method
was invoked. For example, the makeItTalk() method shown
below accepts an Animal reference as a parameter and invokes
talk() on that reference:
class Interrogator {
static void makeItTalk(Animal subject) {
subject.talk();
}
}
At compile time, the compiler doesn't know exactly which class of
object will be passed to makeItTalk() at runtime. It only knows
that the object will be some subclass of Animal. Furthermore, the
compiler doesn't know exactly which implementation of talk()
should be invoked at runtime.
As mentioned above, dynamic binding means the JVM will
decide at runtime which method to invoke based on the class of
the object. If the object is a Dog, the JVM will invoke Dog's
implementation of the method, which says, "Woof!". If the object
is a Cat, the JVM will invoke Cat's implementation of the method,
which says, "Meow!". Dynamic binding is the mechanism that
makes polymorphism, the "subsitutability" of a subclass for a
superclass, possible.
Polymorphism helps make programs more flexible, because at
some future time, you can add another subclass to the Animal
family, and the makeItTalk() method will still work. If, for
example, you later add a Bird class:
class Bird extends Animal {
void talk() {
System.out.println("Tweet, tweet!");
}
}
you can pass a Bird object to the unchanged makeItTalk()
method, and it will say, "Tweet, tweet!".
Getting more polymorphism
Interfaces give you more polymorphism than singly inherited
families of classes, because with interfaces you don't have to
make everything fit into one family of classes. For example:
interface Talkative {
void talk();
}
abstract class Animal implements Talkative {
abstract public void talk();
}
class Dog extends Animal {
public void talk() {
System.out.println("Woof!");
}
}
class Cat extends Animal {
public void talk() {
System.out.println("Meow.");
}
}
class Interrogator {
static void makeItTalk(Talkative subject) {
subject.talk();
}
}
Given this set of classes and interfaces, later you can add a new
class to a completely different family of classes and still pass
instances of the new class to makeItTalk(). For example,
imagine you add a new CuckooClock class to an already existing
Clock family:
class Clock {
}
class CuckooClock implements Talkative {
public void talk() {
System.out.println("Cuckoo, cuckoo!");
}
}
Because CuckooClock implements the Talkative interface, you
can pass a CuckooClock object to the makeItTalk() method:
class Example4 {
public static void main(String[] args) {
CuckooClock cc = new CuckooClock();
Interrogator.makeItTalk(cc);
}
}
With single inheritance only, you'd either have to somehow fit
CuckooClock into the Animal family, or not use polymorphism.
With interfaces, any class in any family can implement Talkative
and be passed to makeItTalk(). This is why I say interfaces give
you more polymorphism than you can get with singly inherited
families of classes.
The 'burden' of implementation inheritance
Okay, my "more polymorphism" claim above is fairly
straightforward and was probably obvious to many readers, but
what do I mean by, "without the burden of multiple inheritance of
implementation?" In particular, exactly how is multiple
inheritance of implementation a burden?
As I see it, the burden of multiple inheritance of implementation
is basically inflexibility. And this inflexibility maps directly to the
inflexibility of inheritance as compared to composition.
By composition, I simply mean using instance variables that are
references to other objects. For example, in the following code,
class Apple is related to class Fruit by composition, because
Apple has an instance variable that holds a reference to a Fruit
object:
class Fruit {
//...
}
class Apple {
private Fruit fruit = new Fruit();
//...
}
In this example, Apple is what I call the front-end class and Fruit
is what I call the back-end class. In a composition relationship,
the front-end class holds a reference in one of its instance
variables to a back-end class.
In last month's edition of my Design Techniques column, I
compared composition with inheritance. My conclusion was that
composition -- at a potential cost in some performance efficiency
-- usually yielded more flexible code. I identified the following
flexibility advantages for composition:


It's easier to change classes involved in a composition
relationship than it is to change classes involved in an
inheritance relationship.
Composition allows you to delay the creation of back-end
objects until (and unless) they're needed. It also allows
you to change the back-end objects dynamically
throughout the lifetime of the front-end object. With
inheritance, you get the image of the superclass in your
subclass object image as soon as the subclass is created,
and it remains part of the subclass object throughout the
lifetime of the subclass.
The one flexibility advantage I identified for inheritance was:

It's easier to add new subclasses (inheritance) than it is to
add new front-end classes (composition), because
inheritance comes with polymorphism. If you have a bit of
code that relies only on a superclass interface, that code
can work with a new subclass without change. This isn't
true of composition, unless you use composition with
interfaces.
In this last flexibilility comparison, however, inheritance is not as
secure as it might seem given its polymorphism advantage. That
last clause above, "unless you use composition with interfaces,"
is very important. Basically, thanks to interfaces, the composition
relationship can also bask in the warm glow of polymorphism.
Here's an example:
interface Peelable {
int peel();
}
class Fruit {
// Return int number of pieces of peel that
// resulted from the peeling activity.
public int peel() {
System.out.println("Peeling is appealing.");
return 1;
}
}
class Apple implements Peelable {
private Fruit fruit = new Fruit();
public int peel() {
return fruit.peel();
}
}
class FoodProcessor {
static void peelAnItem(Peelable item) {
item.peel();
}
}
class Example5 {
public static void main(String[] args) {
Apple apple = new Apple();
FoodProcessor.peelAnItem(apple);
}
}
Given the above set of classes, you could later define a class
Banana like this:
class Banana implements Peelable {
private Fruit fruit = new Fruit();
public int peel() {
return fruit.peel();
}
}
Like Apple, class Banana has a composition relationship with
Fruit. It reuses Fruit's implementation of peel() by explicit
delegation: it invokes peel() on its own Fruit object. But a
Banana object can still be passed to the peelAnItem() method of
class FoodProcessor, because Banana implements the Peelable
interface.
As this example illustrates, interfaces allow you to get the best of
both worlds. You get the flexibility of composition and the
flexibility of polymorphism in one design.
Choosing between composition and inheritance
As I described in last month's article, my basic approach to
choosing between inheritance and composition is that I make
sure inheritance models a permanent is-a relationship. The is-a
relationship means that a subclass is a more specialized form of
a superclass (that the superclass is a more general form of the
subclass). For example, a SavingsAccount is-an Account. I
believe that modeling all (and only) permanent is-a relationships
with inheritance helps maximize the code flexibility, because
doing so gives inheritance a clear meaning that can help other
programmers understand your code.
My main design philosophy is that its primary goal should be to
maximize code flexibility, defined as the ease with which code
can be understood and changed. Although I state in last month's
article that composition in general yields more flexible code than
inheritance, I list reasons that show composition yielding code
that's easier to change, but not necessarily easier to understand.
Inheritance, if you use it just for is-a relationships, gives you the
flexibility advantage that your code becomes easier to
understand.
My feeling, therefore, is that the way to get maximum advantage
of both inheritance and composition in your designs is to ask
yourself if you have a permanent is-a relationship. If so, use
inheritance. If not, use composition.
Interface guidelines
Where, then, do interfaces fit into this picture? As I mention
above, one major benefit of the Java interface is that they give
composition a shot at polymorphism. When you use composition
with interfaces, it becomes as easy to add a new front-end class
(composition) as it is to add a new subclass (inheritance). But
what does this tell us? Should you always use interfaces every
time you use composition? Well, no. Should you avoid using
interfaces in conjunction with single inheritance of class
extension? Certainly not.
As I mentioned at the beginning of this article, it took me a long
time to get the point of interfaces. The epiphany finally came
when I recognized that separation interface and implementation
is one of the primary ideas behind Java in general. The Java
virtual machine (JVM), for example, is an abstract computer that
defines the way your program "interfaces" with the underlying
real computer. A JVM that runs on Windows is one
implementation of that abstract computer. A JVM that runs on the
Macintosh is another. A JVM that runs on your wristwatch is yet
another.
Likewise, the Java APIs are designed not to give you access to
specific capabilities of particular computers and operating
systems, but define abstract interfaces through which your
programs talks to the underlying concrete computer and
operating system, whatever it is. Swing, for example, provides an
interface through which your Java program can create graphical
user interfaces on whatever platform happens to be underneath.
You can even use Swing to create user-interfaces on your
wristwatch, so long as someone has done the work to implement
Swing on your wristwatch.
Separation of interface and implementation is central to Java's
spirit, and the Java interface construct enables you to achieve
this separation in your designs. Two major activities of any
software system design are identifying parts (the subsystems
within a program or system of programs) and specifying the
interfaces between the parts. In designing a Java-based system,
you should use Java interfaces to represent abstract interfaces -the ways in which the parts will interact with each other.
So this is how I ended up thinking about Java's interfaces: as the
preferred means of communicating with the parts of your
program that represent abstractions that may have several
implementations. For example, two parts of a program I describe
in my September Design Techniques installment, "The Event
Generator Idiom", were TelephoneListener and Telephone. In
this design, I decided that the "telephone listener" represented
an abstraction that could have multiple implementations, but that
Telephone did not. Thus, I made Telephone a class that didn't
implement any interfaces, and defined TelephoneListener as an
interface. Telephone, an event source, passed events to
(communicated with) listeners through the TelephoneListener
interface.
I see interfaces as a fundamental tool for achieving flexibility in
the design of Java-based systems. Any class can provide an
implementation of an interface. As long as you don't change the
interface itself, you can make all kind of changes to the
implementing classes, or plug in new classes, without impacting
code that depends only on the interface. Thus, if you have a
subsystem that represents an abstraction that may have multiple
implementations, whether the subsystem is a single object, a
group of objects, an entire Java applet or application, you should
define Java interfaces through which the rest of the world
communicates with that subsystem. When you use interfaces in
this way, you decouple the parts of your system from each other
and generate code that is more flexible: more easily changed,
extended, and customized.
Resources


Bill Venners' next book is Flexible Java
http://www.artima.com/flexiblejava/index.html
Bill Venners just got back from his European bike trip.
Read about it at:
http://www.artima.com/bv/travel/bike98/index.html
















The discussion forum devoted to the material presented in
this article
http://www.artima.com/flexiblejava/fjf/interfaces/index.html
Links to all previous design techniques articles
http://www.artima.com/designtechniques/index.html
Recommended books on Java design
http://www.artima.com/designtechniques/booklist.html
The interfaces chapter from Bill's Course Reader, Objects
and Java
http://www.artima.com/innerjava/webuscript/interfaces.htm
l
A transcript of an e-mail debate between Bill Venners,
Mark Johnson (JavaWorld's JavaBeans columnist), and
Mark Balbe on whether or not all objects should be made
into beans
http://www.artima.com/flexiblejava/comments/beandebate.
html
Object orientation FAQ
http://www.cyberdyne-object-sys.com/oofaq/
7237 Links on Object Orientation
http://www.rhein-neckar.de/~cetus/software.html
The Object-Oriented Page
http://www.well.com/user/ritchie/oo.html
Collection of information on OO approach
http://arkhp1.kek.jp:80/managers/computing/activities/OO
_CollectInfor/OO_CollectInfo.html
Design Patterns Home Page
http://hillside.net/patterns/patterns.html
A Comparison of OOA and OOD Methods
http://www.iconcomp.com/papers/comp/comp_1.html
Object-Oriented Analysis and Design Methods: A
Comparative Review
http://wwwis.cs.utwente.nl:8080/dmrg/OODOC/oodoc/oo.h
tml
Patterns discussion FAQ
http://gee.cs.oswego.edu/dl/pd-FAQ/pd-FAQ.html
Patterns in Java AWT
http://mordor.cs.hut.fi/tik-76.278/group6/awtpat.html
Software Technology's Design Patterns Page
http://www.sw-technologies.com/dpattern/
Previous Design Techniques articles
http://www.javaworld.com/topicalindex/jw-titechniques.html
About the author
Bill Venners has been writing software professionally for 12
years. Based in Silicon Valley, he provides software consulting
and training services under the name Artima Software Company.
Over the years he has developed software for the consumer
electronics, education, semiconductor, and life insurance
industries. He has programmed in many languages on many
platforms: assembly language on various microprocessors, C on
Unix, C++ on Windows, Java on the Web. He is author of the
book: Inside the Java Virtual Machine, published by McGraw-Hill.
Reach Bill at bv@artima.com.
This article was first published under the name Designing with
Interfaces in JavaWorld, a division of Web Publishing, Inc.,
November 1998.
Download