Jera Design : Tech Info : Why a Duck?
An adaptation of a talk given by John Brewer at the April 2001 meeting of the Marin Java Discussion Group.
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.
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();
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();
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.
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.
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.
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.
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.
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.
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.
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.
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.
A machine readable version of the source code from this paper is available for download:
duck.zip (6k)
© 2001 John Brewer. All rights reserved.