Don't Refactor Like Uncle Bob. I'm Begging You

A close reading of Robert Martin's famous refactoring example from Clean Code reveals that the "improved" version is actually worse — introducing mutable state, hidden logic, and reduced readability — and the original method was better all along.

Robert Martin's Clean Code has influenced an entire generation of software developers, and many of its principles are genuinely useful. But its practical examples deserve scrutiny rather than reverence. Chapter 2, on meaningful names, contains a refactoring exercise that I would argue goes in entirely the wrong direction — and teaching it uncritically to beginners does real damage.

Let us look at it closely.

The Original Code

Martin starts with this method, which he presents as in need of improvement:

private void printGuessStatistics(char candidate, int count) {
    String number;
    String verb;
    String pluralModifier;
    if (count == 0) {
        number = "no";
        verb = "are";
        pluralModifier = "s";
    } else if (count == 1) {
        number = "1";
        verb = "is";
        pluralModifier = "";
    } else {
        number = Integer.toString(count);
        verb = "are";
        pluralModifier = "s";
    }
    String guessMessage = String.format(
        "There %s %s %s%s", verb, number, candidate, pluralModifier
    );
    print(guessMessage);
}

It is not beautiful. The three variables declared up top and assigned in branches are a bit awkward. But it is a pure function: given a candidate character and a count, it produces output. Everything it does is visible in one screen. You can understand it completely in about thirty seconds.

Martin's Refactored Version

public class GuessStatisticsMessage {
    private String number;
    private String verb;
    private String pluralModifier;
 
    public String make(char candidate, int count) {
        createPluralDependentMessageParts(count);
        return String.format(
            "There %s %s %s%s", 
            verb, number, candidate, pluralModifier );
    }
 
    private void createPluralDependentMessageParts(int count) {
        if (count == 0) {
            thereAreNoLetters();
        } else if (count == 1) {
            thereIsOneLetter();
        } else {
           thereAreManyLetters(count);
        }
    }
 
    private void thereAreManyLetters(int count) {
        number = Integer.toString(count);
        verb = "are";
        pluralModifier = "s";
    }
 
    private void thereIsOneLetter() {
        number = "1";
        verb = "is";
        pluralModifier = "";
    }
 
    private void thereAreNoLetters() {
        number = "no";
        verb = "are";
        pluralModifier = "s";
    }
}

What Went Wrong

A pure function became a stateful object. The original method took parameters and produced output. The refactored version converts number, verb, and pluralModifier into mutable instance variables. The methods thereAreNoLetters(), thereIsOneLetter(), and thereAreManyLetters() do not return anything — they write to shared state as a side effect. This is the definition of a code smell in most modern style guides.

The parameter handling is logically inconsistent. The data associated with a message — the candidate character and the count — belongs to the object if anything does. Yet candidate is passed as a parameter to make(), while count is passed down through createPluralDependentMessageParts(). The object's own state is populated through side effects from these calls. It is conceptually incoherent.

The chapter is about meaningful names, and the names are not good. make() is one of the vaguest method names in Java. GuessStatisticsMessage mixes a noun and a verb in a way that signals the class has not found its identity. The supposedly expressive method names — thereAreNoLetters() — read more like sentences than identifiers, which crosses a readability line in the other direction.

Logic is now hidden across multiple methods. To understand what happens when count == 0, you must trace through make()createPluralDependentMessageParts()thereAreNoLetters(), at which point you discover that zero is represented as the string "no". In the original, that fact is visible at a glance. The refactoring has taken an obvious thing and buried it.

A Better Direction

If the goal is readability, the simplest improvement is a functional approach that eliminates the awkward late-assignment pattern without introducing any new complexity:

private String generateGuessSentence(char candidate, int count) {
    String verb = count == 1 ? "is" : "are";
    String countStr = count == 0 ? "no" : Integer.toString(count);
    String plural = count == 1 ? "" : "s";
    return String.format(
        "There %s %s %s%s",
        verb, countStr, candidate, plural
    );
}

One method. Pure function. Immutable locals. Everything visible. No state to track.

If a class-based design is genuinely needed — because this message object needs to be passed around, serialized, or extended — here is how to do it properly: put the data in the constructor, and have each method return a value rather than writing to shared fields.

public class GuessStatisticsMessage {
    private final char candidate;
    private final int count;
 
    public GuessStatisticsMessage(char candidate, int count) {
        this.candidate = candidate;
        this.count = count;
    }
 
    public String make() {
        if (count == 0) return thereAreNoLetters();
        if (count == 1) return thereIsOneLetter();
        return thereAreManyLetters();
    }
 
    private String thereAreManyLetters() {
        return String.format("There are %d %ss", count, candidate);
    }
 
    private String thereIsOneLetter() {
        return String.format("There is 1 %s", candidate);
    }
 
    private String thereAreNoLetters() {
        return String.format("There are no %ss", candidate);
    }
}

The object owns its data. Methods return values. State is not mutated through side effects. The class invariants are established at construction and never violated.

The Broader Point

Martin correctly identified that the original method had room for improvement. The direction he chose was not the right one, and the example has been repeated in tutorials, bootcamps, and code reviews for years as if it were gospel.

Clean Code was published in 2008. Software development has changed. The industry has broadly moved toward functional patterns, immutability, and explicit data flow — precisely the things that Martin's refactoring works against. Principles age; specific examples age faster.

Read Martin. Think about what he is trying to achieve. Then develop your own judgment about how to achieve it. Do not cargo-cult the refactoring examples. The original method, for all its awkwardness, was better than what replaced it.