| Paul Haahr / Essays / Java Style / Class structure |
|
|
Class structureWrite documentation comments for all classes and members | |
|
Java's documentation comment facility is a convention for writing comments which can be extracted to create external documentation for a set of classes. At the very least, every class, method, and field in a program should have a documentation comment explaining what it does. Unfortunately, documentation comments come in only one variety. It might make more sense to have different forms of documentation comments, to distinguish between comments for clients of a library from those for programmers working on the library itself. To some degree, this can be obtained by treating documentation comments on public constructs as intended for clients, and use all other documentation comments for internal documentation. Then, when internal documentation is needed for a public class or method, normal comments could follow the documentation comment. I've been somewhat inconsistent on this matter, and tend to put more implementation documentation in public documentation comments than I probably should. In general, I format documentation comments with the leading sequence ``/**'' and trailing sequence ``*/'' on separate lines, and use lines with leading asterisks for the content of the comment. (This is how such comments appear in the JavaSoft books.) But, for comments on members of inner classes and on individual entries in an interface which consists only of a series of constants, I use only single line comments. Thus: | |
|
/**
* Constant values for the three bears.
*/
public interface Bears {
/** Too big constant value. */
int PAPA_BEAR = 1;
/** Too small constant value. */
int BABY_BEAR = -1;
/** Just right constant value. */
int MAMA_BEAR = 0;
};
Group related declarations | |
|
Often, people will put all fields together, then all methods. Or all public members, then protected, then package-accessible, and finally private. This makes reading a class harder, as the reader will have to constantly scroll up and down to make sense of a given routine. As much as possible, I think it's best to put related definitions near each other. Large classes (when they can't be broken into smaller classes) should have their members divided among sections; title-style comments can be used to separate the sections for a reader. Don't use package-qualified names except in import declarationsAn important aspect of Java's package system is that references to classes outside the current package (or the java.lang set of core classes) can be easily identified by reading the list of imports at the beginning of a source file. Unfortunately, this attribute is lost if package-qualified names are used outside of import declarations. The only exception I make to this rule is when a single class needs to use two classes with the same unqualified name from different packages. That is a common case in classes which bridge two packages, for example. Import whole packages only in limited circumstancesBy importing entire packages with the import package.* notation, as opposed to importing individual classes, the actual dependencies from one package to another become hard to see. This becomes especially true when multiple packages are imported in their entirety. However, there are cases where importing whole packages is the right thing. For example, if all code in a given system uses some core classes from a locally defined package, importing all of that package everywhere does little harm; such a foundation can be thought of as an application-specific extension of the java.lang package -- definitions which are necessary for any of the rest of the system. Also, given a package which defines a framework of classes which are meant to subclassed, a package which actually subclasses several of those classes can be thought of as a ``sub-package'' of the original, and it makes sense to import the entire base package. In all cases where entire packages are imported, the message to the reader of the source is ``this class depends on the entirety of that package.'' Don't build circular package relationshipsPackages encapsulate moderate-sized components of a system. The goals of using packages are the same as for any other encapsulation mechanism: to hide implementaion details from clients and to enable reuse. When a program is large enough to be decomposed into multiple packages, the actual partitioning requires some thought and effort. Much could be written about how to structure packages in Java, but I use a simple rule for determining whether package lines are drawn well: if there are circular relationships among packages, the partitioning is not clear and should be rethought. Why do circularities indicate problems? First, it's usually a good idea to think about software as being stratified into levels of concern (or abstraction): the low-level building blocks, the high-level application code, etc. Circularities obscure which level various pieces of the system work at. Second, circularities prevent independent reuse of packages, by tangling up the dependencies. Finally, circularies often make it difficult to decide which package a new class belongs in, because the decision can't be easily made based on which level of abstration the new class presents. If, when writing a program, you find the need for circular references between packages A and B, there are two basic approaches for untangling the circularities:
This rule is, of course, not hard and fast. It is violated, for example, by the core Java libraries, where there are interdependent relationships among classes in java.lang, java.util, and java.io, for example. Don't overuse inheritance (especially subclassing)Many programmers, especially those new to object-oriented programming, will use inheritance simply because they can. This is usually a mistake. Inheritance has its purposes, and when it is the right thing, it is an essential language feature. The cases where I naturally think of using inheritance include:
On the other hand, signs that inheritance is being used inappropriately include having two classes subclass another one yet never being used interchangeably nor sharing method implementations from the superclass. Similarly, inheriting fields which are ignored in subclasses or methods which always signal errors when invoked on a subclass are indicators that classes are partitioned badly. In Java, my rule of thumb is to use interface inheritance if multiple classes need to serve the same role, and class inheritance only when a significant portion of the implementation of the classes can be shared by inheriting methods. The lack of full multiple inheritance in Java makes class inheritance a valuable resource, which may only be spent once per class, where interfaces may be added somewhat freely. If for no other reason, this urges a preference for using interfaces rather than subclasses when possible. (I've started using the pattern of defining both an interface and an abstract class which provides default behavior, for the circumstances where I want an interface that any client can use, but I also want to provide an implementation which can be used. In this case, for an interface named Quux, I'll usually name the class providing default behavior SimpleQuux or BasicQuux.) Don't subclass concrete classesWhen subclassing, it's often unclear what aspects of a superclass are properly inherited and which were dragged along unintentionally. This is greatly complicated by inheriting from classes which are meant to be instantiated in their own right, where some fields, methods, and constructors are typically not relevant for subclasses. Therefore I make a simple rule of only subclassing abstract classes. In other words, I treat all classes as either abstract or final, though actually declaring concrete classes as final should be considered a separate decision. A useful discipline is to initially treat all classes as final. When development calls for subclassing a given class (say Quux) for the first time, change it from final to abstract and create a new final subclass (for example, SimpleQuux) which inherits all the default methods. After working with various subclasses for a little while, if there is behavior which is only inherited by a single subclass (typically, the ``simple'' one), move the implementation to that subclass and make the corresponding method abstract in the superclass. I make an exception to this rule, in general, for exception classes, where I will use both a general exception class for reporting most exceptions, and specialized subclasses when there are particular cases I want to distinguish for use in catch clauses or as declared exceptions. Keep classes, fields, and methods private until they're needed elsewhereInformation hiding is an essential technique for writing large systems and reusable code. By hiding the implementation details of one part of a system, internal changes to it will not break other components which depend on it. And by exposing the minimal amount of information from a given class, we reduce the dependencies that could develop against it. There are many aspects to information hiding, but the central one in Java is the use of access declarations. By making fields and methods private by default and then widening their accessibility as needed (and by not declaring classes public until they are needed outside their package), the most information possible is kept hidden. Using this discipline forces programmers to actively make the decision to expose a feature only when it's needed, when the pros and cons of revealing more of a class can be debated in a real context. | |
|
A different view of the advantage of keeping access as narrow as useful is that, by reducing the exposed interface of a class, the class has fewer features that need to be learned by a potential user, and thus is simpler to understand and reuse. Use final for fields which shouldn't change | |
|
A final declaration on a field is used to ensure that the field is assigned an initial value either in the declaration or in all constructors, and that the field's value is never changed after that point. Typically, an object is composed of mutable state -- fields which may change over the lifetime of the object -- and immutable state -- fields which won't change. It is useful to make this distinction explicit in a program, both to allow a compiler to catch erroneous attempts to set a field and to communicate the intent of the original programmer. Make all non-final fields private | |
|
In Java, if a field is not final and not private, it can be modified by code in another class. Such modifications are often problematic, because they violate the encapsulation and ownership assumptions of the object-oriented style, so my rule of thumb is to disallow them. In general, if I have a field named field which I need to use in other classes, I keep it private but add an accessor method named getField to return its value. If I need to externally modify the value, I add a corresponding setField method. Then, if I need to change how the field is implemented, add a type conversion or assertion check, or track modifications to the field, only a single class needs to be changed. (In fact, where possible, I use the get- and set-methods inside the defining class, because that reduces the number of places which need to be changed within the defining class.) I typically make the accessor methods final unless and until some subclass does need to override them. This can help performance in some environments (by making inlining a simple and correct optimization) and enforces the notion that the accessors are simply controlled-access mechanisms for reading and writing the field. Support anonymous instance creation when sensibleAnonymous instance creation, via Class.newInstance(), is the simplest way to take advantage of one of Java's most powerful features, dynamic class loading. (Until the addition of core reflection to the language, it was the only way.) This encourages Java programmers to write public constructors with empty argument lists, to support dynamic loading. Unfortunately, the addition of a constructor for anonymous instance creation can complicate a class by requiring set-methods for fields which would otherwise just be initialized by a constructor in the non-anonymous case. Typically, doing so will prevent making those fields final. More importantly, it raises questions such as ``what are default values for this field?'' and ``what happens when operations are invoked on an instance where the fields haven't been set?'' Because of these complications, I prefer to use dynamic class loading in very restricted forms. The main style I use is to dynamically load a single class which implements an abstract factory interface (or extands an abstract class) and can be created anonymously. This class then provides factory methods for creating specific instances with the necessary parameters, which are passed directly to constructors. If you find yourself using dynamic loading in many places, it may be more appropriate to use an existing component technology. Java Beans and Enterprise Java Beans provide disciplined mechanisms for managing dynamically-loaded components. |