“Indeed, the ratio of time spent reading versus writing is well over 10 to 1. We are constantly reading old code as part of the effort to write new code. …[Therefore,] making it easy to read makes it easier to write.”
— Robert C. Martin, “Clean Code: A Handbook of Agile Software Craftsmanship”
Clean code is more than just error-free syntax. It’s about:
Before diving into specifics, let’s ponder why clean code is paramount:
Let’s consider a fragment from a hypothetical Java-based e-commerce system:
public List<String> process(List<String> a, Map<String, String> b, String c) {
List<String> d = new ArrayList<>();
for (String e : a) {
if (b.get(e).equals(c) && e.length() > 10) {
d.add(e);
}
}
return d;
}
The purpose of this method is unclear due to poor naming and magic numbers.
Lets look into some clean code practices and try to evolve this piece of code.
“There are only two hard things in Computer Science: cache invalidation and naming things.” — Phil Karlton
Descriptive naming refers to the practice of using names that provide clear insights into the purpose, usage, and implications of a function, variable, class, or any other code entity.
Why is it important?
Originally, our method had the generic name process
and variables like a
, b
, and c
. Lets apply this principle to our code and assign some meaningful names:
public List<String> filterProductIdsByCategory(List<String> productIds, Map<String, String> productCategoryMapping, String category){
List<String> filteredProducts = new ArrayList<>();
for (String productId : productIds) {
if (productCategoryMapping.get(productId).equals(category) && productId.length() > 10) {
filteredProducts.add(productId);
}
}
return filteredProducts;
}
The method name and variable names are now much clearer, making the code’s purpose evident without diving deep into its logic.
Magic numbers are numericals or hard-coded values in your code without a clear explanation or context. The clean coding principle recommends replacing these numbers with named constants.
Why is it important?
We will replace the magic number 10
with a constant named MIN_PRODUCT_ID_LENGTH
. This change makes it clear that we’re enforcing a product ID length condition and offers a single point of change if this criterion needs to be updated.
private static final int MIN_PRODUCT_ID_LENGTH = 10;
public List<String> filterProductIdsByCategory(List<String> productIds, Map<String, String> productCategoryMapping, String category){
List<String> filteredProducts = new ArrayList<>();
for (String productId : productIds) {
if (productCategoryMapping.get(productId).equals(category) && productId.length() > MIN_PRODUCT_ID_LENGTH) {
filteredProducts.add(productId);
}
}
return filteredProducts;
}
Decomposition involves breaking down large methods into smaller, more specific methods.
Why is it important?
Our initial method was doing both filtering by category and validating product ID length. After decomposition:
private static final int MIN_PRODUCT_ID_LENGTH = 10;
public List<String> filterProductIdsByCategory(List<String> productIds, Map<String, String> productCategoryMapping, String category){
List<String> filteredProducts = new ArrayList<>();
for (String productId : productIds) {
if (isProductOfCategory(productId, productCategoryMapping, category) && isValidProductId(productId)) {
filteredProducts.add(productId);
}
}
return filteredProducts;
}
private boolean isProductOfCategory(String productId, Map<String, String> productCategoryMapping, String category) {
return productCategoryMapping.get(productId).equals(category);
}
private boolean isValidProductId(String productId) {
return productId.length() > MIN_PRODUCT_ID_LENGTH;
}
These methods are now modular, with a clear and singular responsibility.
JavaDocs are Java’s built-in documentation tool. They allow developers to write comments for their methods, classes, and variables in a structured manner.
Why is it important?
Lets add JavaDocs for our methods, making it clear what each method does, its parameters, and its return values. This added layer of documentation makes the codebase significantly more approachable for other developers.
private static final int MIN_PRODUCT_ID_LENGTH = 10;
/**
* Filters product IDs by a specified category and validates ID length.
*
* @param productIds list of product IDs.
* @param productCategoryMapping a map of product IDs to categories.
* @param category the category to filter by.
* @return a list of filtered product IDs.
*/
public List<String> filterProductIdsByCategory(List<String> productIds, Map<String, String> productCategoryMapping, String category){
List<String> filteredProducts = new ArrayList<>();
for (String productId : productIds) {
if (isProductOfCategory(productId, productCategoryMapping, category) && isValidProductId(productId)) {
filteredProducts.add(productId);
}
}
return filteredProducts;
}
/**
* Checks if a product belongs to the specified category.
*
* @param productId the product's ID.
* @param productCategoryMapping a map of product IDs to categories.
* @param category the category to check against.
* @return true if the product belongs to the specified category, false otherwise.
*/
private boolean isProductOfCategory(String productId, Map<String, String> productCategoryMapping, String category) {
return productCategoryMapping.get(productId).equals(category);
}
/**
* Validates if the product ID meets length criteria.
*
* @param productId the product's ID.
* @return true if the product ID length is above the minimum threshold, false otherwise.
*/
private boolean isValidProductId(String productId) {
return productId.length() > MIN_PRODUCT_ID_LENGTH;
}
The code snippet we began with has certainly evolved into a more readable and maintainable form, wouldn’t you agree?
While our example was a concise snippet, when diving into more intricate code structures, it’s important to also consider the following practices.
Consistent code structure and adherence to language-specific conventions ensure the codebase is uniform and predictable.
Why is it important?
As per Java conventions, we should ensure that private helper methods are located after public methods and constants are defined at the top of the class. Additionally, consistent spacing, indentation, and brace placement make the code visually clear.
The DRY principle emphasizes the elimination of redundancy in code. Instead of writing the same code in multiple places, it’s recommended to encapsulate it in a singular location and reuse it.
Why is it important?
Though our provided Java snippet was compact, in larger codebases, it’s common to find repeated logic or operations. By applying the DRY principle, we’d encapsulate such repeated logic into separate methods or classes, ensuring that there’s a single source of truth.
For instance, if our system had multiple methods filtering lists based on different criteria but using similar logic, we’d centralize the filtering mechanism and make the criteria a variable or a strategy, ensuring consistency and reducing code repetition.
In the ever-evolving world of software, it’s not just about crafting code — it’s about crafting quality code. By incorporating the principles of clean coding into our daily routine, we pave the way for a more sustainable, maintainable, and collaborative coding environment.
Today’s masterpiece is tomorrow’s legacy. By adhering to these clean code principles, we ensure that our legacy stands the test of time, remaining robust and comprehensible for the developers of the future. Remember, clean code isn’t about taking extra time — it’s about saving countless hours in the future by investing a few more minutes today.