The following points, also motivated by the bug patterns we've discussed, are some important keys to minimize the introduction of bugs into your code. Remember, these methods may be in conflict with the goals of maximizing static type checking, the other side of the good-programming coin.
- Factor out common code. (Say it over and over again—it's that important.)
- Make methods that are purely functional whenever possible.
- Initialize all fields in constructors.
- Throw exceptions when exceptional conditions occur.
- Signal errors as soon as they are discovered.
- Discover errors as soon as possible via parsing, type checking, etc.
- Place assertions into code via casts, assertTrue() methods, documentation, and arguments in the form of documentation.
- Test code as closely to the user-observable state as possible.
Factor Out Common Code
Code that has been copied and pasted is the root of all programming evil. Bugs must be fixed in every copy. Improvements in one copy must similarly be made in all. Programmers trying to understand the code will have to read each copy separately, wasting time. That's why there's a whole bug pattern—the Rogue Tile—dedicated to problems with copy-and-paste code.
As mentioned in the discussion on the Rogue Tile pattern, the Command design pattern is a great way to factor out common code. Similarly, the Visitor design pattern is useful for providing type-specific behavior on a Composite class hierarchy. Both are useful in flushing out Rogue Tiles.
Additionally, various extensions to Java, such as JSR-14, NextGen, and AspectJ, help to factor out common code where it would be otherwise impossible in Java.
Factor Out Common Code
This point just can't be stressed enough.
Make Methods That Are Purely Functional Whenever Possible
Whenever data is mutated, it can be broken. Also, if there are multiple references to the data, modification via one reference can be made without the knowledge or consent of other clients—but those clients might be relying on invariants that are broken when the data is mutated. And, when data is being modified, it can take on inconsistent states, which is dangerous in the context of multithreading.
By making methods that are purely functional (that, in essence, involve no mutation), we avoid all of these problems at once.
Another advantage of purely functional methods is that their behavior (which in this case is merely the values they return in relation to their input) can be understood in isolation. That makes it much easier for other programmers to understand the code; they can absorb a piece of it at a time.
Initialize All Fields in Constructors
Run-on initializers result in NullPointerExceptions, which we hate. Just say no!
Throw Exceptions When Exceptional Conditions Occur
Don't use null flags to signal error conditions. They'll be dereferenced and your program will crash.
Signal Errors as Soon as They're Discovered
The longer the time between when a bug occurs and when it's reported, the more the state can change. The more the state changes, the fewer clues we have to work with.
As a wise man once said, "Fear leads to anger, anger leads to hate, hate leads to suffering." Avoid suffering by nipping problems in the bud.
Discover Errors as Soon as Possible
In order to signal errors as soon as possible, you have to discover them by active programming strategy; you can't wait for them to come to you.
This tenet is especially important in the context of persistent data in which the stakes are higher. If we don't discover a bug immediately, it can linger as a Saboteur.
Parse input and check that it satisfies all required invariants. That'll involve designing Composite class hierarchies specifically to represent parsed data, and parsers to transform input into instances of those classes.
When checking higher-level constraints over the parsed input, the functionality can be put into the constituent classes, but I prefer to keep the logic for such an analysis all in one place by encapsulating it in a Visitor.
Place Assertions into Code
When writing code, try to make sure that the operations you perform are guaranteed to succeed without incident. Then put the argument that convinces you of this fact into the code, in the form of documentation and executable assertions.
Think of the program text itself as a means for arguing for its own correctness. This programming style is sometimes referred to as persuasive programming.
Declaring preconditions, postconditions, and invariants for methods is a good idea. So is documenting the invariants expected to hold in else clauses. One tool to help you check such invariants is iContract.
Test Code as Closely to the User-Observable State as Possible
The longer the distance between what we test and what the user observes, the more room there is for a bug to introduce a Liar View
Related Online Articles:
- Design Patterns for Debugging - Maximizing Static Type Checking
- Bug Pattern in Java - Building Cost-Effective Specifications with Stories
- Design Patterns for Debugging
- Bug Pattern in Java - Other Obstacles to Factoring Out Code.
- Bug Patterns in Java - The Orphaned Thread
- Bug Pattern in Java - Null Pointers Everywhere
- Bug Pattern in Java - Saboteur Data
- Bug Pattern in Java - Debugging & the Development Process
- Bug Pattern in Java - The Broken Dispatch
- Bug Patterns in Java - The Fictitious Implementation
No comment yet. Be the first to post a comment.