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.
