Jera Design : Tech Info : Why a Duck?

Why a Duck?

An Introduction to Polymorphism and Design Patterns

by John Brewer

An adaptation of a talk given by John Brewer at the April 2001 meeting of the Marin Java Discussion Group.

Introduction

What does it mean to be object oriented? There are 3 key aspects of object oriented programming:

Encapsulation means that objects keep their state information private. Rather than directly manipulating an object's data, other objects send requests to the object, in the form of messages, some of which the object may respond to by altering its internal state.

Polymorphism means that objects of different classes can be used interchangeably. This is especially important, as it allows you to hook up classes at a later date in ways you didn't necessarily foresee when those classes were first designed.

Inheritance means that objects of one class can derive part of their behavior from another (base or parent) class. Some object-oriented languages (C++, for example, but not Java) allow multiple inheritance, where objects of one class can derive part or all of their behavior from multiple independent base classes.

You can program using an object-oriented approach in just about any language. For example in C, you can implement encapsulation with pointers to privately defined structs, polymorphism with function pointers, and inheritance by somewhat trickier use of those same function pointers. An object-oriented programming language makes object-oriented programming easier by making encapsulation, polymorphism and inheritance first-class language constructs, usually through some concept of a "class".

This paper deals primarily with polymorphism. Ward Cunningham has said the Polymorphism is the object-oriented programmer's edge. Just as a downhill skier uses the edges of his skis to quickly turn and react to the changing landscape, an O-O developer uses polymorphism judiciously to produce systems that are simple to understand, yet can be rapidly modified in the face of changing requirements.

Starting Out

Let's take a look at a contrived example:

public class Duck1 {
	private String myName;
	
	public Duck1(String theName) {
		myName = theName;
	}
	
	public void quack() {
		System.out.println(myName + ": Quack!");
	}
}

Here we are saying that we have a class called Duck, and that objects of class Duck know how to quack(). Obviously a production Duck class would have additional methods for things like swim(), walk(), and fly(), but quack() is sufficient for our purposes.

Client use of duck would look something like:

	Duck1 duck = new Duck1("Donald");
	duck.quack();

This is fine, as far as it goes. But now let's introduce another related class into the system, a Duck Call:

public class DuckCall1 {
	private String myName;
	
	public DuckCall1(String theName) {
		myName = theName;
	}
	
	public void quack() {
		System.out.println(myName + ": Fake Quack!");
	}
}

Note that, while a DuckCall performs one of the same functions of a Duck, a DuckCall is in no way related to Duck. You can't say a DuckCall is-a Duck, or is-a Bird, or even is-a Animal. A DuckCall is a completely different class that just happens to also know how to quack(). So now we can use Duck like:

	Duck1 duck = new Duck1("Donald");
	duck.quack();

and we can use DuckCall like:

	DuckCall1 duckCall = new DuckCall1("Duck Call");
	duckCall.quack();

What we can't do, however, is use a DuckCall where a Duck is expected:

	Duck1 duck = new DuckCall1("Duck Call");   // Error: Can't convert DuckCall1 to Duck1.
	duck.quack();

Polymorphism

There are occasions where it might come in handy to be able to use Duck and DuckCall interchangeably. In Java the way you say this is to create an interface, like this:

interface Quackable {
	void quack();
}

Essentially, a client of Quackable is saying, "I don't care what you are, as long as you can respond to quack()."

Now we can refactor Duck1 into Duck2. Note the only change is adding an implements clause to the class declaration. The actual body of the class is unchanged.

public class Duck2 implements Quackable {
	private String myName;
	
	public Duck2(String theName) {
		myName = theName;
	}

	public void quack() {
		System.out.println(myName + ": Quack!");
	}
}

Then we add Quackable to DuckCall2

public class DuckCall2 implements Quackable {
	private String myName;
	
	public DuckCall2(String theName) {
		myName = theName;
	}

	public void quack() {
		System.out.println(myName + ": Fake Quack!");
	}
}

Now the client can create a Duck or a DuckCall, and use them interchangeably:

	Quackable quacker;

	quacker = new Duck2("Donald");
	quacker.quack();

	quacker = new DuckCall2("Duck Call");
	quacker.quack();

Factory Method

Even though the client no longer needs to know the exact type of a Quackable in order to call its quack() method, it still needs to know the type of the object to create it. There are cases where we would just as soon insulate the client entirely from the underlying implementation class. To do this, we encapsulate the creation of the object in a method on another class. It's called a Factory Method, because it manufactures an instance of the proper class.

Public class QuackFactory1 {
	public static Quackable createQuacker(String name) {
		return new DuckCall2(name);
	}
}

Now client use of Quackables looks like:

	Quackable quacker = QuackFactory1.createQuacker("Duck Call");
	quacker.quack();

We don't know if QuackFactory returns a Duck or a DuckCall, and for our purposes, we don't care.

Decorator

Now that we have added polymorphism, in the form of the Quackable interface, a large number of patterns become easily reachable. For instance, suppose we wanted to keep track of the number of times quack() was invoked on any Quackable in the system. Rather than modify Duck and DuckCall, we can create a simple wrapper class. This class will implement Quackable, and itself contain a Quackable. When its quack() method is called, it will increment the global quack count and then pass the quack() call on to its contained Quackable:

public class QuackDecorator implements Quackable {
	private Quackable myQuackable;
	
	public QuackDecorator(Quackable theQuackable) {
		myQuackable = theQuackable;
	}
	
	public void quack() {
		ourQuackCount++;
		myQuackable.quack();
	}
	
	private static int ourQuackCount = 0;
	
	public static int getQuackCount() {
		return ourQuackCount;
	}
}

Now, a client just has to wrap each Quackable in a QuackDecorator, and the system will keep an actuate count of the number of times quack() was called.

		Quackable quacker = new QuackDecorator(new Duck2("Donald"));
		quacker.quack();
		
		quacker = new QuackDecorator(new DuckCall2("Duck Call"));
		quacker.quack();
		
		quacker = new Duck2("Daisy");	// OOPS!
		quacker.quack();

		System.out.println("Total # of Quacks: " + QuackDecorator.getQuackCount());

Uh-oh! We forgot to wrap one of the Quackables in a QuackDecorator! Our quack count will be off as a result. This is what happens when you force too much creation work onto the client.

Factory Method Revisited

The solution, however, is simple. First we modify our QuackFactory to return a wrapped Quackable:

public class QuackFactory2 {
	public static Quackable createDuck(String name) {
		return new QuackDecorator(new Duck2(name));
	}
	
	public static Quackable createDuckCall(String name) {
		return new QuackDecorator(new DuckCall2(name));
	}
}

And second, make sure our client only creates instances of Quackables via the QuackFactory:

	Quackable quacker = QuackFactory2.createDuck("Donald");
	quacker.quack();

	quacker = QuackFactory2.createDuckCall("Duck Call");
	quacker.quack();

	quacker = QuackFactory2.createDuck("Daisy");
	quacker.quack();

	System.out.println("Total # of Quacks: " + QuackDecorator.getQuackCount());

In Java you can force the client to use a factory object to create classes by putting the classes to create in the same package as the factory object, and then giving the constructors package scope. Note that this doesn't work if you want to be able to use the Factory to create objects based on classes outside the factory object's package. In the interest of brevity and clarity, the example above doesn't try to enforce factory use.

A common use of Decorator is creating a debug harness, which logs when certain calls on an interface are made. This is quite similar to our use above. Another common use of Decorator is to add some sort of additional functionality to an existing object. For Quackables, we could imagine a Decorator that didn't pass on the quack() call if it was late at night, so as not to annoy the neighbors. Alternatively, we could imagine a Decorator that did something additional on each quack(), either before or after the call to the contained Quackable's quack() method.

Composite

In Decorator, we created an object that implemented an interface and contained another object that implemented that same interface. But why stop with just one? We can create an object that contains an entire collection of Quackables, and calls each one in turn:

import java.util.Vector;
import java.util.Enumeration;

public class QuackComposite implements Quackable {
	private Vector myQuackers = new Vector();
	
	public void addQuacker(Quackable quacker) {
		myQuackers.addElement(quacker);
	}
	
	public void quack() {
		Enumeration enumeration = myQuackers.elements();
		
		while (enumeration.hasMoreElements()) {
			Quackable quacker = (Quackable)(enumeration.nextElement());
			quacker.quack();
		}
	}
}

A client for this might look something like this:

	QuackComposite quackers = new QuackComposite();
	quackers.addQuacker(new Duck2("Donald"));
	quackers.addQuacker(new Duck2("Daisy"));
	quackers.addQuacker(new Duck2("Daffy"));
	quackers.addQuacker(new DuckCall2("Duck Call"));
		
	quackers.quack();

Note how one call to quack() by the client results in 4 quacks printed to standard out. Constructing a QuackComposite is quite complicated, so it would be a good candidate for a creational pattern, either a Factory, as we saw above, or a Builder, which we won't be discussing in the interest of brevity.

Composite is used when you have an object containing a bunch of other objects. For example, an AWT Frame object can contain Panels, Buttons, and TextFields, to name just a few. When you tell the Frame to paint itself, you also typically want all the objects contained in the frame to paint themselves as well, so Frame (like all objects descended from java.awt.Container) keeps a collection of contained objects, and calls their paint() methods when its own paint() method is called.

Composite is also useful for querying groups of objects. For example you could collect Employee objects inside of Department objects that implemented the same interface. You could then query an employee for his salary, or a Department for the total salaries of all of its Employees.

Adapter

Now let's introduce a new class into our system. This one is a Goose:

public class Goose {
	private String myName;
	
	public Goose(String name) {
		myName = name;
	}
	
	public void honk() {
		System.out.println(myName + ": Honk!");
	}
}

Note that a Goose is-not-a Duck. There could be a common base class, such as Waterfowl, but in our implementation there isn't. Right now, we can't use Goose in a place that takes a Quackable, since Goose doesn't expose a quack() method, just a honk() method. That means the following client could will currently generate a compile-time error:

	Quackable goose = new Goose("Gertrude"); // Error: Incompatible type for declaration.
	goose.quack();

This is a good thing. One of the advantages of a type-safe language is that it keeps you from accidentally using an object that doesn't implement the right interface, just as differently shaped electrical plugs keep you from plugging your 110 volt shaver into a 220 volt outlet.

But sometimes, you really do want to plug your 110 volt shaver into a 220 volt outlet, say when you are traveling in Europe where all outlets are 220 volts. In that case, you have to buy an adapter, which both converts the voltage, and makes the plug fit. In object-oriented terms, the voltage adapter plugs into a 220 volt interface (via the plug), and exposes a 110 volt interface (via its socket). The adapter pattern does the same thing for objects.

Let's say we want a Goose to honk() when we tell it to quack(). We can then write a simple adapter class:

public class GooseAdapter implements Quackable {
	private Goose myGoose;
	
	public GooseAdapter(Goose aGoose) {
		myGoose = aGoose;
	}
	
	public void quack() {
		myGoose.honk();
	}
}

All this class does is translate the quack() call into a honk() call. Now, our client can do things like:

	Goose goose = new Goose("Gertrude");
	Quackable wrappedGoose = new GooseAdapter(goose);
	wrappedGoose.quack();

Again, this would be a good place for a Factory Method to create the wrapped Goose without client intervention.

Null Object

What if we wanted to adapt an object to nothing? Say, for instance, that we had a legacy class that required a Quackable as part of its constructor, and then used that Quackable to quack() at interesting times:

	Quackable quackable = new Duck("Daffy");
	Legacy legacy = new Legacy(quackable);

	legacy.doWhatever();	// may call quackable.quack()

Now suppose we have a requirement where we don't want the quacking anymore, but we can't change the code in the legacy class. What can we do? Simply implement a trivial class that implements Quackable, yet does nothing when called:

public class NullQuackable implements Quackable {
	public void quack() {
		return;
	}
}

Now we can create one of these NullQuackables, and pass it into Legacy's constructor:

	Quackable quackable = new NullQuackable();
	Legacy legacy = new Legacy(quackable);
	legacy.doWhatever();	// internal calls to quackable.quack() do nothing

You often find the Null Object pattern used with legacy code, as well as in cases where you find yourself having to do a lot of tests to see if an instance variable is null. In this latter case, you are essentially replacing the conditional:

if (member != null) {

	member.doSomething();
}

with simple polymorphism:

	member.doSomething();

If you end up with a lot of checks for a null member before calling methods, replacing null with Null Object can be a big win.

Combining Patterns

A lot of patterns work well together. Here's a final example that shows Composite, Adapter and Null Object all used together:

	QuackComposite flock = new QuackComposite(); 

	Goose goose = new Goose("Gertrude");
	Quackable wrappedGoose = new GooseAdapter(goose);
	flock.addQuacker(wrappedGoose);

	flock.addQuacker(new Duck2("Donald"));

	flock.addQuacker(new NullQuackable());

	flock.quack();

Again, some sort of Factory pattern would probably be useful here to insulate the client from having to know quite so much about creating the objects inside the flock.

Conclusion

Design patterns allow your system to grow in new directions. Much of the power of design patterns comes from the O-O concept of polymorphism, that is many objects answering to the same interface. By introducing polymorphism into your system at key places, you can make it much more flexible.

This paper has attempted to introduce some basic forms of some of the most commonly used design patterns. Remember that every pattern has many different forms, so don't assume that the only possible way to implement each pattern is the one shown in this paper.

References

Erich Gamma, Richard Helm, Ralph Johnson, and John Vlissides. Design Patterns. Addison-Wesley, Reading, MA, 1995. Also known as the "Gang of Four" book because of its four authors, Design Patterns remains the standard reference on the topic. The best way to work your way through this book is as part of a study group. If you are interested in forming such a group, Joshua Kerievsky of Industrial Logic has created two excellent resources. The first is Pools of Insight: A Pattern Language for Study Groups, which describes how to organize study groups in general. The second is A Learning Guide to Design Patterns which suggests a specific order for approaching the patterns in the Design Patterns book, and provides excellent discussion questions for each pattern.

Martin Fowler, et al. Refactoring: Improving the Design of Existing Code. Addison-Wesley, Reading, MA, 1999. Besides being the definitive work on Refactoring, it also includes a good description of the Null Object pattern. Martin Fowler also maintains an on-line refactoring catalog at refactoring.com.

Source Code

A machine readable version of the source code from this paper is available for download:

duck.zip (6k)


© 2001 John Brewer. All rights reserved.