Columns

Be reflective

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].