The Relativity of Clean Code
If you’ve found your way to this post, there is a good chance you are familiar with computer programming and an even better chance that you have strong opinions on how code should be written. These opinions usually sharpen the moment you are forced to read someone else's work.
If you’ve found your way to this post, there is a good chance you are familiar with computer programming and an even better chance that you have strong opinions on how code should be written. These opinions usually sharpen the moment you are forced to read someone else's work.
For instance, consider the "God Object." Just looking at a snippet like this is enough to make anyone sigh and question their life choices:
public class SuperSystemManager {
public static List<String> D = new ArrayList<>(); // Global-ish state
public String status = "OK";
public void process(int type, String data, boolean save) {
if (type == 1) {
System.out.println("Processing...");
D.add(data);
if (save) {
try {
java.io.FileWriter fw = new java.io.FileWriter("log.txt", true);
fw.write(data);
fw.close();
} catch (Exception e) {
// Silent death: the worst way to handle errors
}
}
} else if (type == 2) {
// Repetitive logic
D.remove(data);
}
// And it just gose on like this for 500 diffrent else if's
}
}
This is a classic example of over-centralization. A single class handles too many unrelated responsibilities and knows far too much about the system. Combined with cryptic naming (what is D?), tight coupling (file I/O buried inside logic), and the "silent death" of an empty catch block, debugging this becomes a nightmare.
Or on the opposite end of the spectrum, maybe you have come across something like this, the "Enterprise Over-Engineer." This developer likely heard a lecture on abstraction and decided to apply it to everything including their morning coffee.
interface IStringOutputProviderFactoryBuilder {
String generate();
}
class AbstractHelloWorldOutputProvider implements IStringOutputProviderFactoryBuilder {
@Override
public String generate() {
return new StringBuilder().append("H").append("e").append("l").append("l").append("o").toString();
}
}
public class Main {
public static void main(String[] args) {
// All this just to print a string
IStringOutputProviderFactoryBuilder factory = new AbstractHelloWorldOutputProvider();
String output = factory.generate();
System.out.println(output);
}
}
Here, every single method is fractured into its own interface or class, creating an endless ladder of files you must climb just to print a string. If you encounter something like this, it's likely because you yelled at a teammate about abstraction or angered an old witch on your way to work. In both cases you should apologies as soon as possible because in both cases, the code can always get worse.
Then of course there is everyone's favorite the lambda of doom!
public void updateUserOrders(List<User> users) {
users.stream()
.filter(u -> u.getOrders() != null)
.forEach(u -> u.getOrders().stream()
.filter(o -> o.getStatus().equals("PENDING"))
.forEach(o -> o.getItems().stream()
.map(item -> {
try {
return repository.findPrice(item.getId())
.map(price -> {
item.setPrice(price);
return item;
}).orElseThrow(() -> new RuntimeException("Price not found"));
} catch (Exception e) {
return null;
}
})
.filter(Objects::nonNull)
.collect(Collectors.toList())
.forEach(i -> System.out.println("Updated: " + i.getName()))
)
);
}
This is a visually exhausting mess. What should ideally be a stateless, elegant operation is actually modifying objects on the fly a fact you only realize after staring at it for five minutes. To top it off, trying to set a breakpoint in this functional soup is an exercise in futility.
What do these three examples have in common? On the surface, they are all difficult to read. But more importantly: they each made perfect sense to the person who wrote them. Each case follows its own consistent internal logic. From the original developer's perspective, the "God Object" was convenient, the "Factory Builder" was extensible, and the "Lambda of Doom" was concise. The mess only appears when someone else tries to decode the intent.
These examples demonstrate the fundamental challenge in programing known as the relativity of clean code. What feels like a "clean" solution in the moment often becomes an "unintentional mask" that hides what the code is actually supposed to do.
What is clean code
Before we dive into the "relativity" of clean code, we must first establish a baseline. Most modern definitions of Clean Code stem from Robert C. Martin’s seminal book, Clean Code: A Handbook of Agile Software Craftsmanship. It is considered essential reading for developers and managers alike, emphasizing several key pillars:
- Readability: Code should be easy for others to understand, even if it means being more verbose than the computer strictly requires.
- Meaningful Naming: Variables, functions, and classes should have names that reveal their intent.
- Single Responsibility: Functions should do one thing, do it well, and do it only.
- Minimal Comments: Comments should explain why (intent/consequences), not what (the obvious). If the code is clear, the comment shouldn't be necessary.
- Robust Error Handling: Errors should be handled without obscuring the main logic.
- The Boy Scout Rule: Always leave the code a little cleaner than you found it.
The Paradox of Over-Application
Our previous examples violate nearly all of these principles. However, in my experience, most poorly written code doesn’t actually stem from ignorance of these rules it stems from the over-application of them.
For example, a "God Object" often begins as a well-intentioned attempt to centralize logic in one place for "readability." When that file eventually balloons to 2,000 lines, the developer tries to fix it by over-abstracting, turning one giant file into twenty 100-line files that all extend each other. In essence, they "fix" the first bad example by turning it into the second. Similarly, developers often create a "Lambda of Doom" in a desperate attempt to force a complex process into a single, "clean" functional statement.
In each case, the developer is trying to treat code like a human language. But because code doesn't obey the laws of linguistics, the result is a confused mess of pseudo-mathematical statements following a logic inherited from written English.
The Cognitive Load of Coding
Programming is a resource-intensive cognitive act. It activates regions of the brain involved in both mathematics and language but doesn't rely solely on either. This makes it uniquely exhausting. Something that is mathematically elegant might be linguistically dense; if you aren't familiar with both the specific syntax and the underlying math of the algorithm, the code becomes a wall of noise.
This is the heart of the relativity of clean code. An experienced programmer can make sense of a complex, lightly-commented block of logic because they have internalized the "math" and "grammar" of the solution. To a beginner, that same block is an impenetrable fortress.
The Solution: Plain Language logic
How do we apply these principles so that the result is understandable to everyone, regardless of their "relative" experience?
The answer doesn't come from a computer science textbook, but from the world of communication. To write truly clean code, we must apply the principles of Plain Language. In the world of writing, Plain Language is design meant to ensure the reader understands the message as quickly, easily, and completely as possible. It is communication that refuses to obscure its own meaning.
What is Plain language communication
Formally, Plain Language is communication designed so the reader can understand it as quickly, easily, and completely as possible. It strives to be accessible, avoiding verbose, convoluted phrasing and unnecessary jargon. In many countries, laws actually mandate that public agencies use plain language to ensure citizens can actually access the services they pay for.
The best concise definition comes from plainlanguage.gov:
"Communication your audience can understand the first time they read or hear it."
However, a more nuanced definition comes from Dr. Robert Eagleson, who explains that plain language is:
"Clear, straightforward expression, using only as many words as are necessary. It is language that avoids obscurity, inflated vocabulary, and convoluted construction. It is not 'baby talk,' nor is it a simplified version of the language."
Plain Language in Action
To see the power of this, look at how we can translate "legalese" or "academic-speak" into something more accessible:
Original (Complex): "This temporary injunction remains in effect against both parties until the final decree of divorce or order of legal separation is entered, the complaint is dismissed, the parties reach agreement, or until the court modifies or dissolves this injunction."
Plain Language (Clear): "You must follow this order until the court changes it, your case is finalized, or you and your spouse reach an agreement."
Or
Original (Complex): "High-quality learning environments are a necessary precondition for facilitation and enhancement of the ongoing learning process."
Plain Language (Clear): "Children need good schools so they can learn well."
The "Audience" Problem in Code
The goal isn't to "dumb down" the content; it’s to respect the reader's time and cognitive load. This brings us back to the relativity of clean code.
When you write in plain language, you must consider the background of your audience. The same applies to your code. A senior developer intimately familiar with a language's nuances might navigate a "Lambda of Doom" with ease. However, if your team includes junior developers or engineers moving over from a language that doesn't support those patterns, that "elegant" code becomes an impenetrable wall.
When logic is obscured by style even a "clean" style the chances of misunderstanding increase. This is where bugs are born. While the standard principles of Clean Code are a great foundation, they clearly aren't a total cure for the friction of different experience levels.
Therefore, I’d like to propose an extension to these principles. Let’s call it Plain Code.
What is Plain Code
Plain Code is clean code that every member of your team can understand the first time they read it. It is code that refuses to obscure its logic through language-specific "magic" or hyper-abstracted patterns.
If Clean Code is the grammar, Plain Code is the intent. It requires you to intentionally account for the gap between your own knowledge and that of your teammates. It’s an extension of "vanilla" Clean Code, built on three additional pillars:
- Readability for the Newcomer: Write as if someone new to the language is reading your code.
- Avoid Feature Obsession: Don't hide logic behind complex, language-specific shorthand. If a specific feature is required, break it into a standalone function that describes its purpose.
- Mind the Knowledge Gap: Don’t assume everyone knows the "why" behind a complex algorithm. Use comments to walk the reader through the logic like a guide, not a lecturer.
Refactoring the "Relativity"
Let’s apply these principles to the mess we found earlier.
1. The God Object: Modular & Meaningful
Initially, our manager was a dumping ground for global state and arbitrary logic. We’ve fixed this by moving data-specific logic into its own interfaces and replacing the endless if-else chain with a clear switch statement. We also moved the File I/O into a dedicated, safe method.
public class SuperSystemManager implements
ProcessProtocolBuffersData, ProcessJSONData, ProcessJavaObjectData{
public void processData(int type, String data, boolean save) {
switch(type){
case ProcessProtocolBuffersData.DATA_TYPE_PROTOCOL_BUFFERS -> processProtocolBuffersData(data,save);
case ProcessJSONData.DATA_TYPE_JSON -> processJSONData(data,save);
case ProcessJavaObjectData.DATA_TYPE_JAVA_OBJECT -> processJavaObjectData(data,save);
default -> processUnknown(type,data,save);
}
}
private void processUnknown(int type, String data, boolean save) {
if(save){
saveUnknownData(data);
} else {
Instant now = Instant.now();
logInfo(now,"Unknown data type: " + type);
}
}
private void saveUnknownData(String data){
boolean appendDataToEndOfFile = true;
try (FileWriter fileWriter = new FileWriter("unknownDataTypeLog.txt",
appendDataToEndOfFile)){
fileWriter.write(data);
}
} catch (Exception exception) {
Instant now = Instant.now();
logError(now,exception);
}
}
}
2. Over-Abstraction: The Power of Records
Instead of a ladder of interfaces just to hold a string, we can use a Java Record. Since Records might be new to some developers, we include a brief comment explaining their purpose.
public class Main {
/**
* A Java Record is a transparent, immutable carrier for data.
* It handles the boilerplate of getters and constructors automatically.
*/
public record Message(String message, Instant timestamp) {}
public static void main(String[] args) {
Message helloWorldMessage = new Message("Hello World", Instant.now());
System.out.println(helloWorldMessage);
}
}
3. The Lambda of Doom: Explicit Logic
The "Lambda of Doom" was a nightmare because it hid its side effects inside a complex stream. We’ve refactored this by extracting the filtering logic into a Predicate and moving the heavy lifting into a standard for-each loop. While less "functional," it is infinitely more debuggable and readable for developers of all levels.
public void updateUserOrders(List<User> listOfUsers) {
// 1. Filter for users who actually have pending orders
Stream<User> streamOfUsers = listOfUsers.stream();
Set<User> setOfUsersWithPendingOrders = streamOfUsers
.filter(getPredicateHasPendingOrders())
.collect(Collectors.toSet());
// 2. Process the updates in a clear, procedural block
updatePricesForUserOrders(setOfUsersWithPendingOrders);
}
public Predicate<User> getPredicateHasPendingOrders(){ //Used to filter out User who have pending orders
return new Predicate<User>() {
@Override
public boolean test(User user) {
boolean hasPendingOrders = false;
if(user != null){
Set<Order> setOfOrders = user.getOrders();
boolean isNotNullOrEmpty = setOfOrders != null && !setOfOrders.isEmpty();
if(isNotNullOrEmpty){
Stream<Order> streamOfOrders = setOfOrders.stream();
hasPendingOrders = streamOfOrders.anyMatch(order -> order.getStatus().equals("PENDING"));
}
}
return hasPendingOrders;
}
};
}
public void updatePricesForUserOrders(Set<User> setOfUserWithPendingOrders){//Logic has been moved from lambda of doom to dedicated for loop
Repository itemRepository = getItemRepository();
boolean isNotEmpty = setOfUserWithPendingOrders != null && !setOfUserWithPendingOrders.isEmpty();
if(isNotEmpty){
for(User nextUser :setOfUserWithPendingOrders){ //all user have already been confirmed to have pending order by our predict
Set<Order> nextSetOfPendingOrders = nextUser.getOrders();
for(Order nextPendingOrder :nextSetOfPendingOrders){
List<Item> nextSetOfItems = nextPendingOrder.getItems();
for(Item nextItem :nextSetOfItems){
if(nextItem != null){ //List can contain nulls so we need to check for them
String nextItemName = nextItem.getItemName();
String nextItemId = nextItem.getItemId();
Double nextNewPrice = itemRepository.findPrice(nextItemId);
if(nextNewPrice != null){
nextItem.setPrice(nextNewPrice);
logInfo("Updated price for " + nextItemName); //each time the price is updated its logged to info out put
} else {
logError("Couldn't find price for " + nextItemName); //each time a null price is found it logs an error
}
}
}
}
}
}
}
Conclusion
By looking at our code through the lens of Plain Language, we ensure that "Clean Code" isn't just a set of rules we follow to satisfy an architect or a book it’s a tool for communication.
In summary, we have explored:
- What Clean Code is (and how it can be over-applied).
- What Plain Language Communication and how it prioritizes the reader.
- The principles of Plain Code to bridge the gap between experience levels.
- How to refactor complex logic into something everyone can understand by implementing those principles.
Stay tuned to see what else we’re cooking up here at The Puttering Dev.