18 Mar 2026 · Updated 30 Mar 2026
Smells and Heuristics - Part 2
Part 2 of the summary of Chapter 17, "Smells and Heuristics" from Robert C. Martin's book "Clean Code".
The second part of this summary of “Clean Code” Chapter 17, “Smells and Heuristics” is all about General do’s and don’ts. Some of them may seem obvious but it is still important to never forget about them.
While reading and summarizing the smells and heuristics for this section, I realized that this would turn into a longer post. I considered splitting the “General” section into two separate posts, but decided to keep it together. Hopefully you won’t find it too long — I believe it’s worth the read.
General
G1: Multiple Languages in One Source File
Keep source files focused on a single language whenever possible. While modern environments allow mixing multiple languages (XML, HTML, JavaScript, etc.) in one file, this creates confusion and reduces code clarity. Minimize language mixing by separating concerns into different files when feasible.
G2: Obvious Behavior Is Unimplemented
Functions and classes should implement all behaviors that users would naturally expect based on their names (The Principle of Least Surprise). If a function claims to convert day names to enums, it should handle abbreviations and case-insensitivity without surprise. Failing to implement expected behavior erodes trust and forces readers to study implementation details instead of relying on intuition.
G3: Incorrect Behavior at the Boundaries
Don’t rely on your intuition. Developers often assume their code works correctly without verifying corner cases. Each boundary condition is a potential failure point that can break the application. Write explicit tests for every edge case rather than trusting assumptions.
G4: Overridden Safeties
Never disable safety mechanisms (compiler warnings, failing tests, verification checks) just for convenience. While there may be rare situations requiring manual overrides, doing so consistently leads to hidden bugs and maintenance nightmares. Disabling safeties is a false shortcut, it creates technical debt and future problems far worse than the inconvenience they prevent.
G5: Duplication
Eliminate all code duplication (the DRY principle: “Don’t Repeat Yourself”). Every duplicate is a missed abstraction opportunity. The most common forms of duplication are: identical code blocks (replace with methods), repeated conditional patterns (use polymorphism), and similar algorithms (use design patterns like TEMPLATE METHOD or STRATEGY). Most design patterns exist precisely to eliminate duplication. Reduce duplication to improve code reusability, reduce errors, and raise abstraction levels.
Nearly every author of software design books writes about this rule and how important this rule is.
G6: Code at Wrong Level of Abstraction
High-level and low-level concepts should be strictly separated. Higher-level abstractions — such as base classes, interfaces, or top-level modules — should encapsulate general behavior only, while lower-level details (constants, implementation-specific utilities, bounded constraints) belong exclusively in derivatives or specialized components. Mixing these levels corrupts the abstraction and undermines the entire design hierarchy.
This principle extends beyond class hierarchies to source files, modules, and components: wherever a conceptual boundary exists, it must be enforced completely.
For a better understanding i keep the example from Oncle Bob:
public interface Stack {
Object pop() throws EmptyException;
void push(Object o) throws FullException;
double percentFull();
class EmptyException extends Exception {}
class FullException extends Exception {}
}
percentFull() is on the wrong level of abstraction. Since not all stack implementations can meaningfully express fullness, the method belongs on a more specific derivative like BoundedStack. Attempting to paper over the mismatch — for instance, by returning 0 for boundless stacks — introduces a lie due to the fact that everything has a memory limit.
The key takeaway: misplaced abstractions cannot be patched with workarounds. Isolating concepts at their correct level of abstraction is one of the most intellectually demanding tasks in software engineering, and no shortcut substitutes for getting it right from the outset.
G7: Base Classes Depending on Their Derivatives
Base classes should never reference their derivatives. The entire point of inheritance is independence, base classes should be high-level and agnostic to low-level implementations. Exceptions exist for fixed, finite state patterns where coupling is acceptable, but generally derivatives and bases should deployable separately. This separation enables modular components that can be updated independently, reducing the impact of changes and simplifying maintenance.
G8: Too Much Information
Minimize what your interfaces expose: fewer methods, fewer variables, and less public state mean lower coupling and better design. Well-designed modules do a lot with a little, poorly designed ones force you to call many methods to accomplish simple tasks. Hide implementation details, utility functions, and temporary variables. Avoid exposing protected members to subclasses unless absolutely necessary. Tight, minimal interfaces reduce complexity and dependencies.
G9: Dead Code
Delete any code that isn’t executed: unreachable conditionals, empty catch blocks, or unused utility methods. Dead code is getting worse over time and starts smelling worse and worse. It creates confusion and maintenance load. When you identify dead code, remove it immediately rather than leaving it as a potential legacy problem.
Maybe you think this smell is similar to F4: Dead Funktions, but this is not only about not called functions. I see F4 more as an subpoint of this.
G10: Vertical Separation
Define variables and functions near where they’re used. Declare local variables just above their first usage to keep scope minimal and improve readability. Similarly, place private function definitions just below their first usage so finding them requires only scanning downward rather than searching the entire class. Minimize vertical distance between definition and usage to enhance code comprehension.
G11: Inconsistency
Consistency in naming and coding conventions reduces confusion and makes software easier to read and maintain.
Once you choose a pattern, such as variable names or method naming styles, apply it uniformly across similar situations. Using the same names for the same kinds of objects and following parallel naming structures for related methods helps ensure that code behaves in line with expectations and avoids surprising other developers.
Consistent practices, even simple ones, significantly improve clarity and maintainability.
G12: Clutter
Clean up everything what is not used. Don’t let unused variables, functions and unnecessary comments clutter your code. Keep your source code clean.
G13: Artificial Coupling
Avoid creating unnecessary dependencies between parts of your code. When unrelated elements, such as general enums or utility functions, are placed inside overly specific classes, the entire system becomes needlessly tied to those classes.
This kind of artificial coupling usually happens when code is placed in a convenient but inappropriate location, which leads to cluttered design and harder maintenance. Instead, take the time to place variables, constants, and functions where they logically belong, ensuring that modules remain independent unless there is a clear, meaningful reason for them to be connected.
G14: Feature Envy
If a method frequently uses another object’s getters and setters to manipulate that object’s internal state, it suggests the logic actually belongs in the other class. The method “envies” the other class’s data and would be more appropriate living there.
It wishes that it were inside that other class so that it could have direct access to the variables it is manipulating.
Keep the behavior close to the data it works with, reducing unnecessary cross‑class dependencies and improving cohesion.
However, there are cases where Feature Envy is justified. If a class is not supposed to know about certain implementation details—such as calculations or data formats—then forcing that logic into the class would conflict with core object-oriented design principles.
G15: Selector Arguments
Everybody knows this boolean argument which decides the behaviour of a function fundamentally. And its always hard do remember, what does true or false mean. It could also be enums or integers it is always a hint that it combines many functions into one.
Don’t avoid splitting a function into serveral small functions. In general it is always better to have many functions that serve exactly one behaviour.
G16: Obscured Intent
Code should be as expressive as possible. Run-on expressions, Hungarian notation, and magic numbers all obscure the author’s intent. Take the time to make the intent of your code visible — no matter how compact a piece of code looks, if it’s impenetrable it’s wrong.
G17: Misplaced Responsibility
Place code where a reader would naturally expect to find it, - guided by the principle of least surprise and the names of functions. The PI constant belongs where the trig functions are declared, OVERTIME_RATE belongs in HourlyPayCalculator. When performance forces a different placement, the function names must still reflect the true responsibility (e.g., computeRunningTotalOfHours instead of saveTimeCard). Its all about intuitivity.
G18: Inappropriate Static
Prefer nonstatic methods over static ones. A static method is only appropriate when it has no chance of needing polymorphic behavior and operates solely on its arguments (e.g., Math.max(a, b)). When in doubt, make the function nonstatic — if you later need different algorithmic variants, you’ll be glad you did.
// Looks reasonable, but blocks polymorphism:
HourlyPayCalculator.calculatePay(employee, overtimeRate)
G19: Use Explanatory Variables
Break complex calculations into intermediate values held in well-named variables. This alone can turn an opaque module transparent. More explanatory variables are generally better than fewer.
Matcher match = headerPattern.matcher(line);
if(match.find())
{
String key = match.group(1);
String value = match.group(2);
headers.put(key.toLowerCase(), value);
}
G20: Function Names Should Say What They Do
If you have to read the implementation to understand what a function does, find a better name. date.add(5) is ambiguous — does it add days, hours, weeks? Does it mutate or return a new value? A name like addDaysTo or daysLater leaves no room for doubt.
G21: Understand the Algorithm
Passing tests is not enough — you must truly understand how your code works. Hacking in flags and conditionals until something “works” produces brittle, untrustworthy code. Refactor the function until it is so clean and expressive that its correctness is obvious.
G22: Make Logical Dependencies Physical
When one module depends on another, that dependency should be explicit, not assumed. If HourlyReporter hardcodes PAGE_SIZE and uses HourlyReportFormatter, it silently assumes HourlyReportFormatter can handle that size — a logical dependency. Physicalize it by adding a getMaxPageSize() method to HourlyReportFormatter and querying it directly, so the assumption is replaced by an explicit contract.
G23: Prefer Polymorphism to If/Else or Switch/Case
Most switch statements are a brute-force default, not the right solution. Before reaching for a switch, consider polymorphism. Apply the ONE SWITCH rule: there should be at most one switch for a given type of selection, and its cases should create polymorphic objects that replace all other switches on the same type elsewhere in the system.
G24: Follow Standard Conventions
Every team should adopt a shared coding standard covering variable declaration placement, naming, brace style, and so on. The code itself should serve as the reference — no separate document needed. What matters is not which convention you choose, but that everyone follows the same one consistently.
G25: Replace Magic Numbers with Named Constants
Raw numbers in code are almost always a bad idea. Hide them behind well-named constants: 86400 becomes SECONDS_PER_DAY, 55 becomes LINES_PER_PAGE. The term “magic number” extends beyond integers — any token whose value is not self-describing (including string literals) should be replaced with a named constant.
G26: Be Precise
Every decision in your code should be deliberate and exact. Don’t use floating-point for currency, don’t assume a query returns only one result, don’t skip null checks, don’t ignore concurrent update risks. Ambiguity and imprecision are the result of laziness or disagreement — eliminate both.
G27: Structure over Convention
Prefer structural enforcement over naming conventions. While good naming helps, it’s less reliable than designs that force correct usage. For example, a switch statement with enums doesn’t guarantee consistent implementation, but an abstract base class does—because concrete classes must implement all required abstract methods.
G28: Encapsulate Conditionals
Boolean logic is hard to understand without the context. When boolean logic is wrapped in a meaningful named function the use of it is much more readable. E.g. shouldBeDeleted(timer) is more intuitive than timer.hasExpired() && !timer.isRecurrent().
G29: Avoid Negative Conditionals
Positive conditionals are more understandable than nagatives. E.g. buffer.shouldCompact() over !buffer.shouldNotCompact().
G30: Functions Should Do One Thing
Resist the temptation to create multi-section functions. When a function performs several operations, break it into smaller, focused functions — each doing exactly one thing. For example:
public void pay() {
for (Employee e : employees) {
if (e.isPayday()) {
Money pay = e.calculatePay();
e.deliverPay(pay);
}
}
}
This should be refactored to:
public void pay() {
for (Employee e : employees)
payIfNecessary(e);
}
private void payIfNecessary(Employee e) {
if (e.isPayday())
calculateAndDeliverPay(e);
}
private void calculateAndDeliverPay(Employee e) {
Money pay = e.calculatePay();
e.deliverPay(pay);
}
G31: Hidden Temporal Couplings
Temporal couplings are often necessary, but don’t hide them. Structure function arguments so the required call order is obvious — a bucket brigade where each function produces a result the next one needs.
public class MoogDiver {
Gradient gradient;
List splines;
public void dive(String reason) {
saturateGradient();
reticulateSplines();
diveForMoog(reason);
}
...
}
A better solution:
public class MoogDiver {
Gradient gradient;
List splines;
public void dive(String reason) {
Gradient gradient = saturateGradient();
List splines = reticulateSplines(gradient);
diveForMoog(splines, reason);
}
...
}
G32: Don’t Be Arbitrary
Every structural decision in your code should have a clear, visible reason. Arbitrary structure invites arbitrary changes. For example, nesting one class inside another with no functional reason confuses others and creates unnecessary coupling:
public class AliasLinkWidget extends ParentWidget
{
public static class VariableExpandingWidgetRoot {
...
...
}
Public utility classes should not be scoped inside another class — keep them at the top level of their package.
G33: Encapsulate Boundary Conditions
Boundary conditions are hard to keep track of. Encapsulate them in a named variable rather than scattering +1 and -1 adjustments throughout your code.
if(level + 1 < tags.length)
{
parts = new Parse(body, tags, level + 1, offset + endTag);
body = null;
}
Introducing a named variable instead:
int nextLevel = level + 1;
if(nextLevel < tags.length)
{
parts = new Parse(body, tags, nextLevel, offset + endTag);
body = null;
}
G34: Functions Should Descend Only One Level of Abstraction
All statements in a function should be at the same level of abstraction — one level below what the function name describes. Mixing levels is a common smell, which is often hard to smell. For example:
public String render() throws Exception
{
StringBuffer html = new StringBuffer("<hr");
if(size > 0)
html.append(" size=\"").append(size + 1).append("\"");
html.append(">");
return html.toString();
}
Separated into distinct abstraction levels. The render method should only construct the HR tag; formatting of the size belongs in hrSize:
public String render() throws Exception
{
HtmlTag hr = new HtmlTag("hr");
if (extraDashes > 0)
hr.addAttribute("size", hrSize(extraDashes));
return hr.html();
}
private String hrSize(int height)
{
int hrSize = height + 1;
return String.format("%d", hrSize);
}
G35: Keep Configurable Data at High Levels
Configuration constants and default values should live at the top of the abstraction hierarchy — visible and easy to change, not buried in low-level functions. Declare them at the class level and pass them down as arguments:
public static void main(String[] args) throws Exception
{
Arguments arguments = parseCommandLine(args);
...
}
public class Arguments
{
public static final String DEFAULT_PATH = ".";
public static final String DEFAULT_ROOT = "FitNesseRoot";
public static final int DEFAULT_PORT = 80;
public static final int DEFAULT_VERSION_DAYS = 14;
...
}
Avoid:
if (arguments.port == 0) // use 80 by default
G36: Avoid Transitive Navigation
Modules should know only about their immediate collaborators (Law of Demeter / “Writing Shy Code”). Avoid chaining like a.getB().getC().doSomething() — it exposes the entire object graph and makes architecture changes expensive across the codebase. Design immediate collaborators to provide all the services callers need directly: myCollaborator.doSomething().