Chapter 17: Smells and Heuristics
The Reference Catalogโ
This final chapter is the most comprehensive in the book โ a catalog of code smells (signs that something is wrong) and heuristics (rules of thumb for making it better). It's meant as a reference to consult when reviewing or refactoring code.
The smells and heuristics are grouped by category.
Commentsโ
C1: Inappropriate Informationโ
Comments should hold only information relevant to the code. Don't use comments for metadata that belongs in version control (change history, author info). Don't explain business decisions that belong in a wiki or ticket system.
C2: Obsolete Commentโ
A comment that has drifted out of sync with the code it describes. If you can't update it, delete it. Stale comments are worse than no comments.
C3: Redundant Commentโ
A comment that describes exactly what the code already says. Noise. Delete it.
// Redundant
i++; // increment i
C4: Poorly Written Commentโ
If a comment is worth writing, write it well. Use correct grammar and spelling. Be brief and precise. Don't ramble.
C5: Commented-Out Codeโ
Delete it immediately. Source control preserves history. Commented-out code accumulates, rots, and confuses readers who wonder if it's important.
Environmentโ
E1: Build Requires More Than One Stepโ
You should be able to build the entire project with a single command. Build scripts that require multiple manual steps are fragile and error-prone.
# Goal: one command builds everything
mvn clean install
E2: Tests Require More Than One Stepโ
Running all the tests should also be a single command. If running tests requires navigating menus, clicking buttons, or running multiple commands, developers will run them less often.
Functionsโ
F1: Too Many Argumentsโ
Functions should have few arguments. Zero is ideal. One or two are fine. Three is borderline. More than three requires a very strong justification. Consider wrapping related arguments in a parameter object.
F2: Output Argumentsโ
Arguments that are modified by a function are confusing. Prefer returning a value over modifying an argument.
// Confusing โ is s being filled? Cleared? Appended to?
appendFooter(s);
// Clear
s = appendFooter(s);
// or: s.appendFooter();
F3: Flag Argumentsโ
Passing a boolean to a function declares that the function does two things. Split it into two functions.
// Bad
render(true); // what does true mean?
// Good
renderForSuite();
renderForSingleTest();
F4: Dead Functionโ
Methods that are never called should be deleted. Don't fear deletion โ source control is your safety net.
Generalโ
G1: Multiple Languages in One Source Fileโ
Source files should contain one language. A Java file with embedded XML, HTML, or SQL strings (especially if long) is hard to read and maintain.
G2: Obvious Behavior Is Unimplementedโ
Following the "Principle of Least Surprise" โ a function named getDayOfWeek(Day.MONDAY) should obviously return DayOfWeek.MONDAY. If the obvious behavior isn't implemented, readers lose trust and must study every detail.
G3: Incorrect Behavior at the Boundariesโ
Don't rely on intuition for edge cases. Write a test for every boundary condition. Algorithms that work in the normal case often fail at min/max values, empty collections, null inputs, or first/last elements.
G4: Overridden Safetiesโ
Don't override safety mechanisms:
- Don't turn off failing tests
- Don't suppress compiler warnings
- Don't ignore exception handling
// Bad โ silently ignoring an exception
try {
riskyOperation();
} catch (Exception e) {
// TODO: handle this
}
G5: Duplicationโ
Every time you see duplicated code, extract it. DRY (Don't Repeat Yourself) is one of the most important principles in software. Common duplication patterns:
- Identical code blocks (extract to method)
- Similar algorithms with minor variations (Template Method pattern)
- Switch/if chains that appear multiple times (polymorphism)
G6: Code at Wrong Level of Abstractionโ
High-level concepts and low-level details should not be mixed in the same class or function.
// Mixed levels
public interface Stack {
void push(Object o);
Object pop();
double percentFull(); // implementation detail leaked into abstract interface
}
G7: Base Classes Depending on Their Derivativesโ
Base classes should not know about their derived classes. If a parent class imports or references a subclass, something is very wrong with the design.
G8: Too Much Informationโ
Well-defined interfaces are small. A class or module with a huge public API is hard to understand and creates tight coupling. Expose the minimum necessary. Hide everything else.
G9: Dead Codeโ
Code that is never executed should be deleted: unreachable branches, uncalled utility functions, unused variables and parameters.
G10: Vertical Separationโ
Variables and functions should be defined close to where they are used. Local variables should be declared just above their first use. Private functions should appear just below their first use.
G11: Inconsistencyโ
If you do something a certain way, do all similar things the same way. getUser() in one place and fetchCustomer() in another for the same kind of operation is confusing.
G12: Clutterโ
Files with unused variables, functions that are never called, comments that add no information โ all of this is clutter. Keep the codebase clean.
G13: Artificial Couplingโ
Things that don't depend on each other should not be forced together. Don't put a general-purpose enum inside a specific class if it has broader use.
G14: Feature Envyโ
A method that spends most of its time manipulating the data of another class probably belongs in that other class (see: Law of Demeter).
// Feature envy โ HourlyPayCalculator is doing Report's job
public class HourlyPayCalculator {
public Money calculateWeeklyPay(HourlyEmployee e) {
int tenthRate = e.getTenthRate().getPennies(); // reaches into HourlyEmployee
int tenthsWorked = e.getTenthsWorked();
// ...
}
}
G15: Selector Argumentsโ
Selector arguments (boolean flags, enums, integers that select behavior) are generally a sign that a function should be split:
// Bad
public int calculatePay(boolean overtime) { ... }
// Good
public int calculateRegularPay() { ... }
public int calculateOvertimePay() { ... }
G16: Obscured Intentโ
Code should reveal its intent. Magic numbers, cryptic names, and clever tricks obscure intent. Always prefer clarity.
G17: Misplaced Responsibilityโ
Code should be placed where the reader expects to find it. Math.PI belongs in Math, not in Geometry or Circle.
G18: Inappropriate Staticโ
Static methods are fine for pure functions that don't operate on any particular instance. But if a method could reasonably be polymorphic โ if you might want to override it in a subclass โ it should not be static.
G19: Use Explanatory Variablesโ
Break up calculations with well-named intermediate variables:
// Hard to parse
if (line.split(",")[3].trim().equals("")) { ... }
// Clear
String city = line.split(",")[3].trim();
if (city.isEmpty()) { ... }
G20: Function Names Should Say What They Doโ
// What does add mean here? Addition? Append? Create?
Date newDate = date.add(5);
// Much clearer
Date newDate = date.addDaysTo(5);
Date newDate = date.increaseByDays(5);
G21: Understand the Algorithmโ
Before you declare a function done, make sure you actually understand it โ not just that the tests pass. "The tests pass but I'm not sure why" is a dangerous state.
G22: Make Logical Dependencies Physicalโ
If a module depends on another, that dependency should be explicit (through method calls, constructor injection, etc.) โ not implicit (via global state or assumed order of initialization).
G23: Prefer Polymorphism to If/Else or Switch/Caseโ
When a switch or if-else chains on type, prefer polymorphism. The exception: if a switch/if chain appears only once and is never duplicated, it may be acceptable.
G24: Follow Standard Conventionsโ
Use the standard Java conventions: class names as nouns, method names as verbs, constants in ALL_CAPS, etc. Don't invent your own naming conventions.
G25: Replace Magic Numbers with Named Constantsโ
// Magic number
if (employees.size() > 25) { ... }
// Named constant
if (employees.size() > MAX_EMPLOYEES_PER_TEAM) { ... }
G26: Be Preciseโ
When you make a decision in code, make it precisely. "Almost correct" is not correct.
- Don't use
floatwhen you needBigDecimalfor money - Don't assume a
Listwhen your algorithm requires ordered unique values (useSortedSet) - Don't assume only one match when there could be multiple
G27: Structure Over Conventionโ
Design decisions enforced by structure are better than those enforced by convention. Use abstract base classes to force implementation โ don't rely on naming conventions that could be ignored.
G28: Encapsulate Conditionalsโ
Extract complex conditionals into named methods:
// Before
if (timer.hasExpired() && !timer.isRecurrent()) { ... }
// After
if (shouldBeDeleted(timer)) { ... }
G29: Avoid Negative Conditionalsโ
Positive conditionals are easier to read:
// Negative โ requires mental negation
if (!buffer.shouldNotCompact()) { ... }
// Positive
if (buffer.shouldCompact()) { ... }
G30: Functions Should Do One Thingโ
If a function does more than one thing, extract it into multiple functions.
G31: Hidden Temporal Couplingsโ
When functions must be called in a certain order, make that order explicit:
// Bad โ implicit order dependency
dive();
checkForObstacles();
// ... must be called in this order but nothing enforces it
// Good โ explicit via return value threading
Oxygen oxygen = dive();
UnderwaterMap map = checkForObstacles(oxygen); // takes previous result
G32: Don't Be Arbitraryโ
Every structural decision in code should have a reason. If you can't articulate why you made a choice, reconsider it.
G33: Encapsulate Boundary Conditionsโ
Boundary conditions are tricky. Encapsulate them so they appear in one place:
// Before โ +1 scattered everywhere
if (level + 1 < tags.length) { ... }
nextLevel = level + 1;
subList = list.subList(level + 1, list.size());
// After โ encapsulated
int nextLevel = level + 1;
if (nextLevel < tags.length) { ... }
subList = list.subList(nextLevel, list.size());
G34: Functions Should Descend Only One Level of Abstractionโ
Each function should be one level of abstraction below its name, and should contain statements at the same level of abstraction.
G35: Keep Configurable Data at High Levelsโ
Constants and configuration values that define default behaviors should be at the top of the call hierarchy โ easy to find and change.
G36: Avoid Transitive Navigation (Law of Demeter)โ
A module should not reach through one object to get to another:
// Bad โ transitive navigation (train wreck)
a.getB().getC().doSomething();
// Good โ each object talks only to its immediate neighbors
a.doSomething(); // a internally delegates to b and c as needed
Java-Specific Smellsโ
J1: Avoid Long Import Lists with Wildcardsโ
Use specific imports:
// Unclear โ what's being imported from java.util?
import java.util.*;
// Clear
import java.util.List;
import java.util.Map;
import java.util.HashMap;
J2: Don't Inherit Constantsโ
Don't implement an interface just to access its constants (the Constant Interface Antipattern). Use import static instead.
// Bad โ inheriting just to get constants
class Foo implements Constants { ... }
// Good
import static com.example.Constants.MAX_SIZE;
J3: Constants vs. Enumsโ
Prefer enums over constants for type safety:
// Old style โ not type-safe
public static final int MONDAY = 1;
// Modern Java โ type-safe, self-documenting
public enum Day { MONDAY, TUESDAY, WEDNESDAY, ... }
Namesโ
N1: Choose Descriptive Namesโ
Names should describe intent precisely. Take time to choose good names โ it pays off many times over.
N2: Choose Names at the Appropriate Level of Abstractionโ
Don't use implementation detail names in high-level code:
// Too specific โ reveals implementation detail at abstract level
interface Modem { void dial(String phoneNumber); } // what about TCP/IP modems?
// Better โ abstracts to the concept
interface Modem { void connect(String connectionString); }
N3: Use Standard Nomenclature Where Possibleโ
Use names from the domain and from established patterns. A Factory creates things. A Decorator wraps things. Readers recognize these immediately.
N4: Unambiguous Namesโ
Avoid names that can be interpreted multiple ways. If the name requires the reader to look at the code to understand it, it's not good enough.
N5: Use Long Names for Long Scopesโ
The longer the scope, the more descriptive the name should be. i is fine for a 3-line loop. It's confusing in a 50-line method.
N6: Avoid Encodingsโ
Don't encode type or scope information into names. Hungarian notation, member prefixes (m_), interface prefixes (I) โ all noise in modern Java.
N7: Names Should Describe Side Effectsโ
Names should describe everything a function, variable, or class is or does:
// Bad โ the name hides the side effect (creating the output stream)
public ObjectOutputStream getOos() throws IOException {
if (oos == null) oos = new ObjectOutputStream(socket.getOutputStream()); // side effect!
return oos;
}
// Better
public ObjectOutputStream createOrReturnOos() throws IOException { ... }
Testsโ
T1: Insufficient Testsโ
A test suite should cover every condition that might fail. Test until you're confident there are no remaining bugs โ then add one more test for good measure.
T2: Use a Coverage Toolโ
Coverage tools reveal untested paths. Aim for high coverage, but remember: 100% coverage doesn't mean 100% correct.
T3: Don't Skip Trivial Testsโ
Trivial tests are easy to write and document behavior clearly. Write them.
T4: An Ignored Test Is a Question About an Ambiguityโ
If you @Ignore a test, that's usually a sign of ambiguous requirements. Resolve the ambiguity.
T5: Test Boundary Conditionsโ
Most bugs live at the boundaries: min/max, empty/full, first/last, null/non-null. Test them explicitly.
T6: Exhaustively Test Near Bugsโ
When you find a bug, test the surrounding area thoroughly. Bugs cluster.
T7: Patterns of Failure Are Revealingโ
Failed tests often reveal patterns โ e.g., all failures happen when input is null, or on certain days of the week. These patterns point to root causes.
T8: Test Coverage Patterns Can Be Revealingโ
Similar to T7 โ looking at which lines of production code are exercised (or not) by failing tests helps diagnose root causes.
T9: Tests Should Be Fastโ
Slow tests are skipped. Slow tests that are skipped don't catch regressions. Keep tests fast.
Key Takeawaysโ
This chapter is a reference. The key meta-lessons:
- Code smells are signals, not verdicts. They indicate where to look, not necessarily what to do.
- Heuristics require judgment. Apply them with wisdom, not mechanically.
- This list is not exhaustive. Develop your own smell-detection instincts through experience and code review.
- Clean code is a practice, not a destination. Smells accumulate; clearing them requires continuous attention.