This
appendix contains suggestions to help guide you in performing low-level program
design, and in writing
code.
Naturally, these are guidelines and not
rules. The idea is to use them as inspirations, and to remember that there are
occasional situations where you need to bend or break a rule.
[ Add Comment ]
Design
Elegance always
pays off. In the short term it might seem like it takes much longer to come
up with a truly graceful solution to a problem, but when it works the first time
and easily adapts to new situations instead of requiring hours, days, or months
of struggle, you’ll see the rewards (even if no one can measure them). Not
only does it give you a program that’s easier to build and debug, but
it’s also easier to understand and maintain, and that’s where the
financial value lies. This point can take some experience to understand, because
it can appear that you’re not being productive while you’re making a
piece of code elegant. Resist the urge to hurry; it will only slow you down.
[ Add Comment ]
First
make it work, then make it fast. This is true even if you are certain that a
piece of code is really important and that it will be a principal bottleneck in
your system. Don’t do it. Get the system going first with as simple a
design as possible. Then if it isn’t going fast enough, profile it.
You’ll almost always discover that “your” bottleneck
isn’t the problem. Save your time for the really important stuff.
[ Add Comment ]
Remember
the “divide and conquer” principle. If the problem you’re
looking at is too confusing, try to imagine what the basic operation of the
program would be, given the existence of a magic “piece” that
handles the hard parts. That “piece” is an object—write the
code that uses the object, then look at the object and encapsulate its
hard parts into other objects, etc.
[ Add Comment ]
Separate
the class creator from the class user (client programmer). The class
user is the “customer” and doesn’t need or want to know
what’s going on behind the scenes of the class. The class creator must be
the expert in class design and write the class so that it can be used by the
most novice programmer possible, yet still work robustly in the application.
Library use will be easy only if it’s transparent.
[ Add Comment ]
When
you create a class, attempt to make your names so clear that comments are
unnecessary. Your goal should be to make the client programmer’s
interface conceptually simple. To this end, use method overloading when
appropriate to create an intuitive, easy-to-use interface.
[ Add Comment ]
Your
analysis and design must produce, at minimum, the classes in your system, their
public interfaces, and their relationships to other classes, especially base
classes. If your design methodology produces more than that, ask yourself if
all the pieces produced by that methodology have value over the lifetime of the
program. If they do not, maintaining them will cost you. Members of development
teams tend not to maintain anything that does not contribute to their
productivity; this is a fact of life that many design methods don’t
account for.
[ Add Comment ]
Automate
everything.Write the test code first (before you write the class),
and keep it with the class. Automate the running of your tests through a
makefile or similar tool. This way, any changes can be automatically verified by
running the test code, and you’ll immediately discover errors. Because you
know that you have the safety net of your test framework, you will be bolder
about making sweeping changes when you discover the need. Remember that the
greatest improvements in languages come from the built-in testing provided by
type checking, exception handling, etc., but those features take you only so
far. You must go the rest of the way in creating a robust system by filling in
the tests that verify features that are specific to your class or program.
[ Add Comment ]
Write
the test code first (before you write the class) in order to verify that your
class design is complete. If you can’t write test code, you
don’t know what your class looks like. In addition, the act of writing the
test code will often flush out additional features or constraints that you need
in the class—these features or constraints don’t always appear
during analysis and design. Tests also provide example code showing how your
class can be used.
[ Add Comment ]
All
software design problems can be simplified by introducing an extra level of
conceptual indirection. This fundamental rule of software
engineering[85]
is the basis of abstraction, the primary feature of object-oriented programming.
[ Add Comment ]
An
indirection should have a meaning (in concert with guideline 9). This
meaning can be something as simple as “putting commonly used code in a
single method.” If you add levels of indirection (abstraction,
encapsulation, etc.) that don’t have meaning, it can be as bad as not
having adequate indirection.
[ Add Comment ]
Make
classes as atomic as possible. Give each class a single, clear purpose. If
your classes or your system design grows too complicated, break complex classes
into simpler ones. The most obvious indicator of this is sheer size: if a class
is big, chances are it’s doing too much and should be broken
up. Clues to suggest redesign of a class
are: 1) A complicated switch statement: consider
using polymorphism. 2) A large number of methods
that cover broadly different types of operations: consider using several
classes. 3) A large number of member variables
that concern broadly different characteristics: consider using several classes.
[ Add Comment ]
Watch
for long argument lists. Method calls then become difficult to write, read,
and maintain. Instead, try to move the method to a class where it is (more)
appropriate, and/or pass objects in as arguments.
[ Add Comment ]
Don’t
repeat yourself. If a piece of code is recurring in many methods in derived
classes, put that code into a single method in the base class and call it from
the derived-class methods. Not only do you save code space, you provide for easy
propagation of changes. Sometimes the discovery of this common code will add
valuable functionality to your interface.
[ Add Comment ]
Watch
for switch statements or chained if-else clauses. This is
typically an indicator of type-check coding, which means you are choosing
what code to execute based on some kind of type information (the exact type may
not be obvious at first). You can usually replace this kind of code with
inheritance and polymorphism; a polymorphic method call will perform the type
checking for you, and allow for more reliable and easier extensibility.
[ Add Comment ]
From
a design standpoint, look for and separate things that change from things that
stay the same. That is, search for the elements in a system that you might
want to change without forcing a redesign, then encapsulate those elements in
classes. You can learn significantly more about this concept in Thinking in
Patterns with Java, downloadable at www.BruceEckel.com.
[ Add Comment ]
Don’t
extend fundamental functionality by subclassing. If an interface element is
essential to a class it should be in the base class, not added during
derivation. If you’re adding methods by inheriting, perhaps you should
rethink the design.
[ Add Comment ]
Less
is more. Start with a minimal interface to a class, as small and simple as
you need to solve the problem at hand, but don’t try to anticipate all the
ways that your class might be used. As the class is used, you’ll
discover ways you must expand the interface. However, once a class is in use you
cannot shrink the interface without disturbing client code. If you need to add
more methods, that’s fine; it won’t disturb code, other than forcing
recompiles. But even if new methods replace the functionality of old ones, leave
the existing interface alone (you can combine the functionality in the
underlying implementation if you want). If you need to expand the interface of
an existing method by adding more arguments, create an overloaded method with
the new arguments; this way you won’t disturb any existing calls to the
existing method.
[ Add Comment ]
Read
your classes aloud to make sure they’re logical. Refer to the
relationship between a base class and derived class as “is-a” and
member objects as “has-a.”
[ Add Comment ]
When
deciding between inheritance and composition, ask if you need to upcast to the
base type. If not, prefer composition (member objects) to inheritance. This
can eliminate the perceived need for multiple base types. If you inherit, users
will think they are supposed to upcast.
[ Add Comment ]
Use
data members for variation in value and method overriding for variation in
behavior. That is, if you find a class that uses state variables along with
methods that switch behavior based on those variables, you should probably
redesign it to express the differences in behavior within subclasses and
overridden methods.
[ Add Comment ]
Watch
for overloading. A method should not conditionally execute code based on the
value of an argument. In this case, you should create two or more overloaded
methods instead.
[ Add Comment ]
Use
exception hierarchies—preferably derived from specific appropriate
classes in the standard Java exception hierarchy. The person catching the
exceptions can then catch the specific types of exceptions, followed by the base
type. If you add new derived exceptions, existing client code will still catch
the exception through the base type.
[ Add Comment ]
Sometimes
simple aggregation does the job. A “passenger comfort system” on
an airline consists of disconnected elements: seat, air conditioning, video,
etc., and yet you need to create many of these in a plane. Do you make private
members and build a whole new interface? No—in this case, the components
are also part of the public interface, so you should create public member
objects. Those objects have their own private implementations, which are still
safe. Be aware that simple aggregation is not a solution to be used often, but
it does happen.
[ Add Comment ]
Consider
the perspective of the client programmer and the person maintaining the
code. Design your class to be as obvious as possible to use. Anticipate the
kind of changes that will be made, and design your class so that those changes
will be easy.
[ Add Comment ]
Watch
out for “giant object syndrome.” This is often an affliction of
procedural programmers who are new to OOP and who end up writing a procedural
program and sticking it inside one or two giant objects. With the exception of
application frameworks, objects represent concepts in your application, not the
application.
[ Add Comment ]
If
you must do something ugly, at least localize the ugliness inside a class.
[ Add Comment ]
If
you must do something nonportable, make an abstraction for that service and
localize it within a class. This extra level of indirection prevents the
nonportability from being distributed throughout your program. (This idiom is
embodied in the Bridge Pattern).
[ Add Comment ]
Objects
should not simply hold some data. They should also have well-defined
behaviors. (Occasionally, “data objects” are appropriate, but only
when used expressly to package and transport a group of items when a generalized
container is innappropriate.)
[ Add Comment ]
Choose
composition first when creating new classes from existing classes. You
should only used inheritance if it is required by your design. If you use
inheritance where composition will work, your designs will become needlessly
complicated.
[ Add Comment ]
Use
inheritance and method overriding to express differences in behavior, and fields
to express variations in state. An extreme example of what not to do is
inheriting different classes to represent colors instead of using a
“color” field.
[ Add Comment ]
Watch
out for variance. Two semantically different objects may have
identical actions, or responsibilities, and there is a natural temptation to try
to make one a subclass of the other just to benefit from inheritance. This is
called variance, but there’s no real justification to force a
superclass/subclass relationship where it doesn’t exist. A better solution
is to create a general base class that produces an interface for both as derived
classes—it requires a bit more space, but you still benefit from
inheritance, and will probably make an important discovery about the design.
[ Add Comment ]
Watch
out for limitation during inheritance. The clearest designs add new
capabilities to inherited ones. A suspicious design removes old capabilities
during inheritance without adding new ones. But rules are made to be broken, and
if you are working from an old class library, it may be more efficient to
restrict an existing class in its subclass than it would be to restructure the
hierarchy so your new class fits in where it should, above the old class.
[ Add Comment ]
Use
design patterns to eliminate “naked functionality.” That is, if
only one object of your class should be created, don’t bolt ahead to the
application and write a comment “Make only one of these.” Wrap it in
a singleton. If you have a lot of messy code in your main program that creates
your objects, look for a creational pattern like a factory method in which you
can encapsulate that creation. Eliminating “naked functionality”
will not only make your code much easier to understand and maintain, it will
also make it more bulletproof against the well-intentioned maintainers that come
after you.
[ Add Comment ]
Watch
out for “analysis paralysis.” Remember that you must usually
move forward in a project before you know everything, and that often the best
and fastest way to learn about some of your unknown factors is to go to the next
step rather than trying to figure it out in your head. You can’t know the
solution until you have the solution. Java has built-in firewalls; let
them work for you. Your mistakes in a class or set of classes won’t
destroy the integrity of the whole system.
[ Add Comment ]
When
you think you’ve got a good analysis, design, or implementation, do a
walkthrough. Bring someone in from outside your group—this
doesn’t have to be a consultant, but can be someone from another group
within your company. Reviewing your work with a fresh pair of eyes can reveal
problems at a stage when it’s much easier to fix them, and more than pays
for the time and money “lost” to the walkthrough process.
[ Add Comment ]
Implementation
In general,
follow the Sun coding conventions. These are available
at java.sun.com/docs/codeconv/index.html
(the code in this book follows these conventions as much as I was able). These
are used for what constitutes arguably the largest body of code that the largest
number of Java programmers will be exposed to. If you doggedly stick to the
coding style you’ve always used, you will make it harder for your reader.
Whatever coding conventions you decide on, ensure they are consistent throughout
the project. There is a free tool to automatically reformat Java code at:
home.wtal.de/software-solutions/jindent.
[ Add Comment ]
Whatever
coding style you use, it really does make a difference if your team (and even
better, your company) standardizes on it. This means to the point that
everyone considers it fair game to fix someone else’s coding style if it
doesn’t conform. The value of standardization is that it takes less brain
cycles to parse the code, so that you can focus more on what the code means.
[ Add Comment ]
Follow
standard capitalization rules. Capitalize the first letter of class names.
The first letter of fields, methods, and objects (references) should be
lowercase. All identifiers should run their words together, and capitalize the
first letter of all intermediate words. For
example: ThisIsAClassName thisIsAMethodOrFieldName Capitalize
all the letters of staticfinal primitive identifiers that
have constant initializers in their definitions. This indicates they are
compile-time constants. Packages are a special
case—they are all lowercase letters, even for intermediate words. The
domain extension (com, org, net, edu, etc.) should also be lowercase. (This was
a change between Java 1.1 and Java 2.)
[ Add Comment ]
Don’t
create your own “decorated” private data member names. This is
usually seen in the form of prepended underscores and characters. Hungarian
notation is the worst example of this, where you attach extra characters that
indicate data type, use, location, etc., as if you were writing assembly
language and the compiler provided no extra assistance at all. These notations
are confusing, difficult to read, and unpleasant to enforce and maintain. Let
classes and packages do the name scoping for you.
[ Add Comment ]
Follow
a “canonical form” when creating a class for general-purpose
use. Include definitions for equals( ), hashCode( ),
toString( ), clone( ) (implement Cloneable), and
implement Comparable and Serializable.
[ Add Comment ]
Use
the JavaBeans “get,” “set,” and “is” naming
conventions for methods that read and change private fields, even if
you don’t think you’re making a JavaBean at the time. Not only does
it make it easy to use your class as a Bean, but it’s a standard way to
name these kinds of methods and so will be more easily understood by the reader.
[ Add Comment ]
For
each class you create, consider including a static public test( )
that contains code to test that class. You don’t need to remove the
test code to use the class in a project, and if you make any changes you can
easily rerun the tests. This code also provides examples of how to use your
class.
[ Add Comment ]
Sometimes
you need to inherit in order to access protected members of the base
class. This can lead to a perceived need for multiple base types. If you
don’t need to upcast, first derive a new class to perform the protected
access. Then make that new class a member object inside any class that needs to
use it, rather than inheriting.
[ Add Comment ]
Avoid
the use of final methods for efficiency purposes. Use final
only when the program is running, but not fast enough, and your profiler has
shown you that a method invocation is the bottleneck.
[ Add Comment ]
If
two classes are associated with each other in some functional way (such as
containers and iterators), try to make one an inner class of the other. This
not only emphasizes the association between the classes, but it allows the class
name to be reused within a single package by nesting it within another class.
The Java containers library does this by defining an inner Iterator class
inside each container class, thereby providing the containers with a common
interface. The other reason you’ll want to use an inner class is as part
of the private implementation. Here, the inner class beneficial for
implementation hiding rather than the class association and prevention of
namespace pollution noted above.
[ Add Comment ]
Anytime
you notice classes that appear to have high coupling with each other, consider
the coding and maintenance improvements you might get by using inner
classes. The use of inner classes will not uncouple the classes, but rather
make the coupling explicit and more convenient.
[ Add Comment ]
Don’t
fall prey to premature optimization. This way lies madness. In particular,
don’t worry about writing (or avoiding) native methods, making some
methods final, or tweaking code to be efficient when you are first
constructing the system. Your primary goal should be to prove the design, unless
the design requires a certain efficiency.
[ Add Comment ]
Keep
scopes as small as possible so the visibility and lifetime of your objects are
as small as possible. This reduces the chance of using an object in the
wrong context and hiding a difficult-to-find bug. For example, suppose you have
a container and a piece of code that iterates through it. If you copy that code
to use with a new container, you may accidentally end up using the size of the
old container as the upper bound of the new one. If, however, the old container
is out of scope, the error will be caught at compile-time.
[ Add Comment ]
Use
the containers in the standard Java library. Become proficient with their
use and you’ll greatly increase your productivity. Prefer ArrayList
for sequences, HashSet for sets, HashMap for associative arrays,
and LinkedList for stacks (rather than Stack) and queues.
[ Add Comment ]
For
a program to be robust, each component must be robust. Use all the tools
provided by Java: access control, exceptions, type checking, and so on, in each
class you create. That way you can safely move to the next level of abstraction
when building your system.
[ Add Comment ]
Prefer
compile-time errors to run-time errors. Try to handle an error as close to
the point of its occurrence as possible. Prefer dealing with the error at that
point to throwing an exception. Catch any exceptions in the nearest handler that
has enough information to deal with them. Do what you can with the exception at
the current level; if that doesn’t solve the problem, rethrow the
exception.
[ Add Comment ]
Watch
for long method definitions. Methods should be brief, functional units that
describe and implement a discrete part of a class interface. A method that is
long and complicated is difficult and expensive to maintain, and is probably
trying to do too much all by itself. If you see such a method, it indicates
that, at the least, it should be broken up into multiple methods. It may also
suggest the creation of a new class. Small methods will also foster reuse within
your class. (Sometimes methods must be large, but they should still do just one
thing.)
[ Add Comment ]
Keep
things as “private as possible.” Once you publicize an
aspect of your library (a method, a class, a field), you can never take it out.
If you do, you’ll wreck somebody’s existing code, forcing them to
rewrite and redesign. If you publicize only what you must, you can change
everything else with impunity, and since designs tend to evolve this is an
important freedom. In this way, implementation changes will have minimal impact
on derived classes. Privacy is especially important when dealing with
multithreading—only private fields can be protected against
un-synchronized use.
[ Add Comment ]
Use
comments liberally, and use the javadoc comment-documentation syntax to
produce your program documentation. However, the comments should add geniune
meaning to the code; comments that only reiterate what the code is clearly
expressing are annoying. Note that the typical verbose detail of Java class and
method names reduce the need for as many comments.
[ Add Comment ]
Avoid
using “magic numbers”—which are numbers hard-wired into
code. These are a nightmare if you need to change them, since you never know if
“100” means “the array size” or “something else
entirely.” Instead, create a constant with a descriptive name and use the
constant identifier throughout your program. This makes the program easier to
understand and much easier to maintain.
[ Add Comment ]
When
creating constructors, consider exceptions. In the best case, the
constructor won’t do anything that throws an exception. In the next-best
scenario, the class will be composed and inherited from robust classes only, so
they will need no cleanup if an exception is thrown. Otherwise, you must clean
up composed classes inside a finally clause. If a constructor must fail,
the appropriate action is to throw an exception, so the caller doesn’t
continue blindly, thinking that the object was created correctly.
[ Add Comment ]
If
your class requires any cleanup when the client programmer is finished with the
object, place the cleanup code in a single, well-defined method—with a
name like cleanup( ) that clearly suggests its purpose. In addition,
place a boolean flag in the class to indicate whether the object has been
cleaned up so that finalize( ) can check for “the death
condition” (see Chapter 4).
[ Add Comment ]
The
responsibility of finalize( ) can only be to verify “the death
condition” of an object for debugging. (See Chapter 4.) In special
cases, it might be needed to release memory that would not otherwise be released
by the garbage collector. Since the garbage collector might not get called for
your object, you cannot use finalize( ) to perform necessary
cleanup. For that you must create your own “cleanup” method. In the
finalize( ) method for the class, check to make sure that the object
has been cleaned up and throw a class derived from RuntimeException if it
hasn’t, to indicate a programming error. Before relying on such a scheme,
ensure that finalize( ) works on your system. (You might need to
call System.gc( ) to ensure this behavior.)
[ Add Comment ]
If
an object must be cleaned up (other than by garbage collection) within a
particular scope, use the following approach: Initialize the object and, if
successful, immediately enter a try block with a finally clause
that performs the cleanup.
[ Add Comment ]
When
overriding finalize( ) during inheritance, remember to call
super.finalize( ). (This is not necessary if Object is
your immediate superclass.) You should call super.finalize( ) as the
final act of your overridden finalize( ) rather than the
first, to ensure that base-class components are still valid if you need them.
[ Add Comment ]
When
you are creating a fixed-size container of objects, transfer them to an
array—especially if you’re returning this container from a
method. This way you get the benefit of the array’s compile-time type
checking, and the recipient of the array might not need to cast the objects in
the array in order to use them. Note that the base-class of the containers
library, java.util.Collection, has two toArray( ) methods to
accomplish this.
[ Add Comment ]
Choose
interfaces over abstract classes. If you know something is
going to be a base class, your first choice should be to make it an
interface, and only if you’re forced to have method definitions or
member variables should you change it to an abstract class. An
interface talks about what the client wants to do, while a class tends to
focus on (or allow) implementation details.
[ Add Comment ]
Inside
constructors, do only what is necessary to set the object into the proper
state. Actively avoid calling other methods (except for final
methods) since those methods can be overridden by someone else to produce
unexpected results during construction. (See Chapter 7 for details.) Smaller,
simpler constructors are less likely to throw exceptions or cause problems.
[ Add Comment ]
To
avoid a highly frustrating experience, make sure that there is only one
unpackaged class of each name anywhere in your classpath. Otherwise, the
compiler can find the identically-named other class first, and report error
messages that make no sense. If you suspect that you are having a classpath
problem, try looking for .class files with the same names at each of the
starting points in your classpath. Ideally, put all your classes within
packages.
[ Add Comment ]
Watch
out for accidental overloading. If you attempt to override a base-class
method and you don’t quite get the spelling right, you’ll end up
adding a new method rather than overriding an existing method. However, this is
perfectly legal, so you won’t get any error message from the compiler or
run-time system—your code simply won’t work correctly.
[ Add Comment ]
Watch
out for premature optimization. First make it work, then make it
fast—but only if you must, and only if it’s proven that there is a
performance bottleneck in a particular section of your code. Unless you have
used a profiler to discover a bottleneck, you will probably be wasting your
time. The hidden cost of performance tweaks is that your code becomes less
understandable and maintainable.
[ Add Comment ]
Remember
that code is read much more than it is written. Clean designs make for
easy-to-understand programs, but comments, detailed explanations, and examples
are invaluable. They will help both you and everyone who comes after you. If
nothing else, the frustration of trying to ferret out useful information from
the online Java documentation should convince you.
[ Add Comment ]