Become a Better Programmer: Clean Code Principles with Java Examples
When I’m on either side of the interview table, the topic of clean code principles invariably comes up. To me, writing code is not just a task — it’s an art.
Like a masterful painting, well-crafted code is a harmonious blend of precision, clarity, and beauty. It adheres to principles that make it easy to read and understand, much like the clear strokes of a paintbrush that give life to a canvas.
Just as artists pour their vision and soul into their creations, programmers infuse their code with creativity and passion.
When written with care, code not only serves its intended purpose but stands as a testament to the elegance and artistry of logical expression.
I strongly believe the ability to write clean code is what differentiates a good programmer from a great one.
One of the book that surfaces up when talking about is Robert C. Martin’s seminal work, “Clean Code: A Handbook of Agile Software Craftsmanship”.
I’ll try to summary, explore and discuss the book’s aim of promoting the clean code: core principles of writing clean, maintainable code using Java as our programming language of choice.
BUT I strongly recommend you to pick up the book and read!
Meaningful Names
The name of a variable, function, or class should tell you exactly why it exists, what it does, and how it is used.
Choosing the right name for a variable, function, or class is fundamental for readability and maintainability of code. A name should be descriptive enough that another developer can easily infer its purpose without having to rely on comments.
Let’s analyse the given Java code snippets to highlight the importance of using meaningful names:
For example, the following code declares a variable for first name:
String fn; // first name
On the first look, the variable fn
seems too abbreviated. It might make sense to the original developer, but to others, it might not be immediately apparent, especially if you don't see the comment.
The situation deteriorates further as we see:
String ln; // last name
String fn2 //full name - first name + last name
int a; // age
List<String> h = new ArrayList<>(); // hobbies
The names are ambiguous: fn
and fn2
are too close in name. It's easy to confuse one for the other. What's more, fn2
is not only a poor name choice because of its similarity to fn
, but it's also misleading. A reader might initially think it's another form of a first name, not a combination of first and last names.
Similarly, variable name for age lacks clarity: a
gives no indication that it's meant to represent an age. Similarly, h
doesn't make it evident that it's a list of hobbies.
Better naming would be:
String firstName;
String lastName;
String fullName; // or combinedName
int age;
List<String> hobbies = new ArrayList<>();
With these names:
- We don’t need comments to understand what each variable represents.
- The chance of confusion or misunderstanding is significantly reduced.
- The code becomes self-documenting, a key aspect of clean code.
Meaningful naming may seem like a trivial task, but it’s an art that requires thought and practice. As Robert C. Martin puts it in his book “Clean Code”, “You should name a variable using the same care with which you name a first-born child.” By investing in better naming conventions today, we save countless hours of debugging and misinterpretation down the road.
Functions Should Do One Thing
One of the fundamental principles of clean code is that functions should do only one thing.
This principle echoes the Single Responsibility Principle (SRP) which is commonly associated with object-oriented design, but it’s just as relevant to functions.
The core idea is simplicity; when a function does just one thing, it’s easier to understand, test, and maintain.
Look at an example of submitting a movie review by a user.
The submit method given below take care of everything — right from validating the review to the saving and updating the database to alerting the UI — all in one single method. This is shown in the following code snippet — the “doItAllMethod”:
public void submit(Review review){
// code to validate review
// code to save to the database
// code to update the database
// code to alert loggedin users
// code to notify UI
}
The function itself could be growing into four or five A4 pages!
The issues with this approach:
- Difficult to Read: As the function grows, it becomes harder to understand. The longer the function — the complex it gets. This complexity can lead to overlooked bugs.
- Testing Challenges: Testing such a method becomes cumbersome. As this method is a “doItAll” type of method, it would be hard to nitpick the testable functions alone. For instance, if you wanted to test just the validation logic, you’d end up inadvertently testing database updates and UI notifications as well.
- Harder to Modify: Business logic is expected to change. What if you need to change the way reviews are validated? There’s a risk of inadvertently affecting the database or UI code.
Instead, let’s rewrite (refactor) the code with with Single Responsibility:
public void submit(Review review){
validateReview(review);
saveAndUpdateDatabase(review);
alertUsers(review);
notifyUI(review);
}
By refactoring the original function into smaller ones, each with a clear, singular responsibility, we gain the benefits of improved readability and easier testing.
If the method of alerting users changes, you can modify the alertUsers
function without touching the validation, database, or UI code, thus promoting modularity.
Finally, as the application grows, it’s easier to add, modify, or remove features as and when needed — thus making the code scale.
Writing functions that do one thing doesn’t necessarily mean they should be short, but rather that they should have a singular focus.
By adhering to this principle, we can produce code that’s more readable, maintainable, easy testable and robust.
As Robert C. Martin aptly states in “Clean Code”, “The reason we write functions is to decompose a larger concept into a set of smaller ones.” And doing that properly can make all the difference in software craftsmanship.
I will also add “Shorter Functions” too, to this list. It’s much easier to read and understand a function that fits on one screen.
Consider Comments as an Apology
If you feel the need to add a comment to explain what a piece of code does, consider refactoring the code to make its purpose clearer.
Look at the code snippet here, where a user is checked if they are eligible for a discout. If they are indeed eligible, the next line will fetch the discount amount:
// Check if the user is eligible for a discount
if (u.isEligible()) {
// if the user is eligible, check the eligbile amount
double d = u.disc();
}
As you can see, the code is full of comments. The developer who wrote it is trying hard to explain to the next guy about what that piece of code does.
Rather than putting effort in commenting the code, why can’t we change the code and instead express our intent using the code’s variables and method names and logic and etc?
That’s exactly what the below snippet demonstrates:
if (user.isEligibleForDiscount()) {
double discountAmount = user.getDiscountAmount();
}
The code is self explanatory now!
Correct Error Handling
In software development, handling errors gracefully is as essential as writing the main logic of the application.
When it comes to clean code, one of the guiding principles is that a function should either handle an error or execute logic, but not both. This approach increases clarity and ensures a function adheres to the Single Responsibility Principle.
Using exceptions rather than return codes is a widely recommended practice for several reasons:
- Separation of Concerns: Exceptions allow you to separate the normal execution path from the error handling path.
- Improved Readability: With return codes, every operation might be followed by error-checking logic, cluttering the main flow.
- Flexibility: Exceptions can be propagated up the call stack, giving higher-level functions a chance to handle them or add additional context.
Suppose we have a list of strings representing numerical values, and we want to convert these strings into integers and calculate their sum. However, some strings might be non-numeric, and we need to handle such cases gracefully.
Let’s create a method convertStringToInt
that’d return -1
for invalid numbers:
public int convertStringToInt(String s) {
try {
return Integer.parseInt(s);
} catch (NumberFormatException e) {
return -1; // this mixes error signaling with actual data
}
}
// Test
List<String> numbers = Arrays.asList("1", "2", "apple", "4");
int sum = numbers.stream()
.map(this::convertStringToInt)
.filter(num -> num != -1)
.mapToInt(Integer::intValue)
.sum();
What we’ve done here is to think that if NumberFormatException will send a -1 as the signal that an error occurred. Using -1
as an error signal can be problematic because genuinely the dataset may contain -1
, which is a valid number in our dataset. The error is silenced. We don’t know if there’s a wrong value in our list.
Instead of silencing the error and returning incorrect signal, the recommend approach is to throw the exception for the client to deal:
public int convertStringToInt(String s) throws NumberFormatException {
return Integer.parseInt(s);
}
Probably the client in this case could catch the exception, as demonstrated below:
// Error handling in Client's program
List<String> numbers = Arrays.asList("1", "2", "apple", "4");
int sum;
try {
sum = numbers.stream()
.mapToInt(this::convertStringToInt)
.sum();
} catch (NumberFormatException e) {
System.err.println("Error: Encountered an invalid number: " + e.getMessage());
// Handle the error as appropriate
}
In this instance, the error handling is separated from the main logic. We immediately become aware of any non-numeric values in our list. You can use Java Streams to collect all erroneous entries, not just halt at the first one. This can be achieved by custom collectors or by partitioning data based on validity.
Leverage Optional
for error handling
As a side note we can leverage Optional
to represent a value that might be absent, leading to cleaner and more expressive code. For instance, the conversion method can return Optional<Integer>
, where an empty Optional indicates an error.
The enhanced error handling code for the above requirement using Optional is given below:
// We use Optional.empty to return "error"
public Optional<Integer> convertStringToInt(String s) {
try {
return Optional.of(Integer.parseInt(s));
} catch (NumberFormatException e) {
return Optional.empty();
}
}
List<String> numbers = Arrays.asList("1", "2", "apple", "4");
// Calculate the sum of valid numbers
int sum = numbers.stream()
.map(this::convertStringToInt)
.filter(Optional::isPresent)
.mapToInt(Optional::get)
.sum();
// Collect invalid numbers to a local List
List<String> invalidNumbers = numbers.stream()
.filter(s -> !convertStringToInt(s).isPresent())
.collect(Collectors.toList());
// And run through the invalid numbers list
if (!invalidNumbers.isEmpty()) {
System.err.println("Error: Encountered invalid numbers: " + String.join(", ", invalidNumbers));
// Further handle the error if needed
}
By returning an Optional<Integer>
, the function clearly communicates that the result might be absent. There’s no need to use potentially misleading error codes. Using the Optional::isPresent
and Optional::get
methods, we can safely check and retrieve values. Errors are handled in a clean, Java 8 idiomatic way. The stream processing is clear, and invalid numbers are easily collected for later handling.
Functions Without Side Effects
In the realm of functional programming and clean code, functions that don’t have side effects are often touted as ideal. These are called “pure” functions.
Pure functions always produce the same result given the same input and have no side effects.
- With no hidden changes happening in the background, the code’s behavior is predictable, making it easier to understand and debug.
- Pure functions are much easier to test since you only need to consider their input and output without worrying about external states.
- And obviously, without side effects, functions can safely run in parallel without risking unwanted state changes.
Essentially, we should strive to have functions not having any hidden side effects. They should not modify any states or values outside their own scope.
Let’s illustrate this with an example using a simple User
class and a function to update a user's age.
With Side Effects (Not Recommended): Here, the updateAge
method modifies an external list, which is a hidden side effect.
// User class
class User {
String name;
int age;
public static List<User> updatedUsers = new ArrayList<>();
public void updateAge(int newAge) {
this.age = newAge;
updatedUsers.add(this); // Yikes! side effect - we are adding the same class!
}
}
// Test code
User user1 = new User("Alice", 25);
// This will add user1 to the updatedUsers list,
// a hidden side effect.
user1.updateAge(26);
Why don’t we do this: instead of modifying the user in place, we return a new User
instance with the updated age.
// User class
class User {
final String name; // immutable fields
final int age;
public User withUpdatedAge(int newAge) {
// No side effect, returns a new User instance.
return new User(this.name, newAge);
}
}
// Test Code
User user1 = new User("Alice", 25);
// the user1 instance remains unchanged
// updatedUser is a new instance
User updatedUser = user1.withUpdatedAge(26);
By avoiding side effects in the function, we ensure that the original User
object remains unchanged, adhering to the immutability principle. The function is now pure; its outcome only depends on its inputs, and it doesn’t modify any external state.
Designing functions without side effects in mind makes the software more maintainable, testable, and less prone to bugs.
It might require a shift in thinking, especially if coming from a traditional imperative background, but the benefits in code quality and maintainability are well worth the effort.
Wrap up
Clean code is more than just aesthetics; it’s about efficiency, maintainability, and scalability. By applying the Clean Code principles, not only will your code be easier to understand and debug, but you will also become a more skilled and thoughtful programmer.
The more you invest yourself in learning these concepts and put them in practice, the better your programming life is! And the next code who’s gonna read your code will bow to you for sure :)
While I try to summarise the clean code priciplese inspired by Robert C. Martin’s principles, it’s just a summary. There’s a ton of material, lot more depth to each of these topics in the original book. I highly recommend you get some time to read through it! It’s a one-of-the-must-haves for any software developer
Recommended Reads:
1. Clean Code: A Handbook of Agile Software Craftsmanship” by Robert C. Martin - Understand the deeper nuances of clean code from the master himself.
2. Refactoring: Improving the Design of Existing Code” by Martin Fowler - Dive into the art of improving code structure without altering its external behaviour.
Disclaimer: This article may contain Amazon affiliate links. As an Amazon Associate, I earn from qualifying purchases. This means that if you click on an affiliate link and make a purchase, I may earn a small commission at no extra cost to you.