Columns
Be reflective
- By Dwight Deugo
- April 1, 2002
Given a choice, I would always choose a reflective designer/developer to join
one of my projects. As Stanford University computer science professor, researcher
and author Terry Winograd puts it, a reflective designer is "someone who
is driven by practical concerns, but who is also able to step back for a moment
and reflect on what works, what doesn't work and why." I would add to this
that a reflective designer/developer carefully considers how their designs and
code will be used by others.
Consider, for example, developing a class to represent a deck of cards called
CardDeck, requiring two behaviors: shuffle and draw. Draw
returns the Card conceptually positioned at the top of the deck, while
shuffle places all 52 Cards back into the CardDeck and
arranges them randomly. A designer could use an array of integers of length
52 to represent the internal structure of the CardDeck along with a marker
indicating which position in the array represents the top, initially set to
0. Draw could increment the marker by one and return the corresponding
Card represented by the number found in the array at the corresponding
position, returning null if the CardDeck is empty. Shuffle resets
the marker to the top of the deck and re-initializes the array with random numbers
between 1 and 52, using each number in the range only once.
One approach to filling the array in shuffle is to grab Donald Knuth's
"The Art of Computer Programming, Volume 2" (Reading Mass.; Addison
Wesley, 1981) from your shelf and implement a stream of pseudo-random numbers
using a 48-bit seed that is modified using a linear congruential formula in
a loop. However, you would find the shuffle method to be long and complicated.
Knowing good object-oriented practices, you should re-factor shuffle
into several smaller methods that shuffle can invoke. However, looking
at the responsibilities for CardDeck, one now finds behaviors such as
setSeed(int), next() and nextInRange(int,int). One passes
setSeed(int), a seed integer that is required by the pseudo-random algorithm
implemented in the method next. Next returns a floating point
number in the range 0.0-1.0, used by the method nextInRange. NextInRange
returns an integer in the range 1-52, used to fill in the array of integers
representing the Cards.
In hindsight, none of these behaviors belongs in CardDeck and they might
confuse other developers who try to use or change CardDeck. The problem
is that CardDeck has captured two abstractions. The first is a deck of
cards; the second is a random number generator.
A better solution is to separate the abstractions into two classes and have
the first class delegate to the second. RandomNumberGenerator has the
behaviors setSeed(int), next() and nextInRange(int,int),
as well as the internal integer seed variable supporting them. The CardDeck
still has the behaviors draw and shuffle, with the internal integer
marker and the Card array supporting them. However, shuffle now
uses the services of RandomNumberGenerator. Each class supports a smaller
interface than the one original, and their behaviors and internal structures
are focused on the specific abstractions, making them both easier to use and
understand. The earlier re-factoring exercise has also helped to keep the implementation
sizes down and the behaviors at reasonable complexity levels.
Objects often have similar responsibilities. How many times have you implemented
an object that needed to sort a list of objects, required a random number, or
needed to maintain and process information about a person's address, client
or company? Without good browsing skills or access to a good IDE that supports
searching, as well as the ability to copy and paste, developers often choose
one of two techniques for developing new objects and their responsibilities.
Either they develop every behavior from scratch, or they copy the implementation
from a previous object into the new one. In either case, the result is the same.
The same behavior or code segment is located in two or more places and causes
an update problem. In the copy-and-paste scenario, provided there is sufficient
commenting, updating one implementation may draw the developer's attention to
the fact that another update is required. However, when everything is developed
from scratch, no such forwarding exists. To make things worse, multiple developers
have spent time doing the same work, which their managers will not appreciate.
Consider another example. Say that in addition to needing a deck of cards,
one also needed a Die. The Die has the behavior role, returning
a number between 1 and 6. If both the Die and CardDeck support
their own abstractions and that of a random number generator, there is a problem.
If you change the random number generator algorithm in either one, there is
a good chance you need to change both. Not only does this duplicate the effort
required to implement the Die and CardDeck, it also increases
the maintenance effort. Effectively, one change needs to be done twice.
As before, the solution is to separate the abstractions into three classes
-- Die, CardDeck and RandomNumberGenerator -- and to have
Die and CardDeck delegate the responsibility of generating random
numbers to the RandomNumberGenerator. To change the algorithm for generating
random numbers because, for example, a new one is faster or "more"
random, you need only alter the appropriate RandomNumberGenerator methods.
Die and CardDeck automatically incorporate those changes because
they delegate to the same object.
This design makes use of three general heuristics. The first is: Don't position
identical behaviors in two or more places. This leads to an obvious maintenance
problem. If a developer changes one, they must also do the same for all others
or they will inadvertently introduce bugs into the application. The second heuristic
is to have many classes, each having a small number of responsibilities, rather
than a few classes doing everything. The third heuristic is to keep the sizes
of a class' responsibilities small. All of these heuristics fall under the general
category of not re-inventing the wheel. If some other class knows how to do
something, use it. Delegate responsibility.
You must always remember that you are usually not the first person to develop
a class. There is a long line of developers before you that have implemented
classes with similar, if not identical, responsibilities. I don't know how many
times I have seen a "new" implementation of a Sorter, RandomNumberGenerator,
Company, Profile, Broker or Name object. The reality
of object-oriented design is that many good designs, frameworks and patterns
already exist. It may sound strange, but I would rather have members of my development
team use other developers' classes than write their own. Every line of code
they don't write is one less line that could contain a bug the team has to fix.
The conflict is that you need to develop new classes, which involves coding;
but if you write less code, you create fewer bugs.
To resolve the conflict, you can have your classes delegate their responsibilities
to other developers' classes. Let the bugs show up in their code. If your classes
work well, you look good. If your classes don't work because of a problem with
a class you delegated responsibility to, it is someone else's problem. You still
look good. In addition, using other people's classes encourages them to produce
better quality code. Strong, identifiable code ownership usually brings quality
with it. No one wants to be the one responsible for inferior code. And, if bugs
do occur and are fixed, more developers benefit. Who said object-oriented design
and programming was difficult?
Taking the approach one step further in our example, you could search the existing
classes in the standard Java packages and find that a random number generator
class -- called java.util.Random -- already exists. Rather than having
to write, maintain, update and debug my own RandomNumberGenerator class,
I can remove it completely and have Die and CardDeck delegate
to java.util.Random. Die and CardDeck still work the same,
but I am now responsible for one less class.
As a manager, you struggle to find developers you can trust to help you with
your work. As a developer, you should struggle to find classes you can trust
to help you with your development. The search is worth the effort, because no
one and no class should work in isolation. The reward is not having to write
and maintain as much code. Think about it.
About the Author
Dwight Deugo is a professor of computer science at Carleton University in Ottawa, Ontario. Dwight has been an editor for SIGS and 101communications publications, and serves as chair of the Java Programming track at the SIGS Conference for Java Development. He can be reached via e-mail at [email protected].