Clean Code — Writing Readable and Maintainable Code

Jawlon
7 min read5 days ago

--

Robert C. Martin in 2020 (Wikipedia)

Robert C. Martin (also known as Uncle Bob), a pioneer in software development, is a founder of the Agile Manifesto and a strong advocate of clean code and Test-Driven Development (TDD). With over 50 years of experience, he has written several influential books on software engineering, including Clean Code: A Handbook of Agile Software Craftsmanship, The Clean Coder and Clean Architecture.

The page of the book (Clean Code: A Handbook of Agile Software Craftsmanship)

Clean Code is a highly regarded book by Robert C. Martin focused on the principles and practices of writing cleaner, more maintainable code. The book emphasizes how small changes in coding habits can lead to better readability, easier maintenance, and fewer bugs.

The code examples provided demonstrate Clean Code principles across multiple programming languages, including Kotlin, PHP, and TypeScript.

Here are the main rules and principles:

Meaningful Names

  • Use descriptive and meaningful names for variables, functions, and classes.
  • Avoid using one-letter variables or non-descriptive names.
  • Function and class names should clearly convey their purpose.

Bad Example:

val d = 30 // what is 'd'?

Good Example:

val daysSinceLastUpdate = 30

Explanation: The variable name daysSinceLastUpdate gives a clear idea of what the variable is used for.

Functions Should Be Small

  • Keep functions short and focused on doing one thing.
  • A function should only do one thing and do it well.
  • Avoid long parameter lists — if you have too many, consider refactoring.

Bad Example:

function processOrder($order) {
if ($order->isValid()) {
$total = $order->calculateTotal();
processPayment($order, $total);
$order->status = "completed";
sendConfirmation($order);
}
}

Good Example:

function processOrder($order) {
if ($order->isValid()) {
$total = calculateOrderTotal($order);
processPayment($order, $total);
completeOrder($order);
}
}

function calculateOrderTotal($order) {
// calculation logic
}

function completeOrder($order) {
$order->status = "completed";
sendConfirmation($order);
}

Explanation: The good example breaks down the large function into smaller, focused functions.

Single Responsibility Principle (SRP)

  • Each class or function should have only one reason to change.
  • Group functionality together logically, so changes to one part don’t affect unrelated parts.

Bad Example:

class UserService {
fun registerUser(user: User) {
// logic to register user
}

fun sendWelcomeEmail(user: User) {
// logic to send email
}
}

Good Example:

class UserService {
fun registerUser(user: User) {
// logic to register user
}
}

class EmailService {
fun sendWelcomeEmail(user: User) {
// logic to send email
}
}

Use Comments Sparingly

  • Code should be self-explanatory, reducing the need for comments.
  • If you need comments, your code might be unclear.
  • Write comments that explain “why” something is done, not “what” the code does.

Bad Example:

// Incrementing i by 1
i++;

Good Example:

i++; // No need for a comment; it's clear what this does

Better Example with Comments on “Why”:

// Retry the operation in case of network failure.
retryOperation();

Explanation: Unnecessary comments are avoided. Only explain “why” something is done if it’s not obvious.

DRY Principle (Don’t Repeat Yourself)

  • Avoid duplicating code. If you find repeated logic, abstract it into a function or class.
  • Repeated code increases the chance of bugs and makes maintenance harder.

Bad Example:

fun calculateDiscount(price: Double): Double {
return price * 0.9 // 10% discount
}

fun calculateSpecialDiscount(price: Double): Double {
return price * 0.85 // 15% discount
}

Good Example:

fun calculateDiscount(price: Double, discountRate: Double): Double {
return price * discountRate
}

Explanation: The discount calculation logic is centralized, avoiding repetition.

Use Descriptive Function Names

  • Functions should describe what they do. If it’s hard to name a function, it might be doing too much.
  • Example: calculateOrderTotal() is better than calculate().

Bad Example:

function make() {
// some logic
}

Good Example:

function sendOrderConfirmationEmail() {
// some logic
}

Explanation: The function name clearly states its purpose.

Avoid Magic Numbers and Strings

  • Don’t use unexplained numbers or string values in your code. Instead, use named constants or enums to improve readability.
  • Example: MAX_RETRY_COUNT = 5 is clearer than just 5.

Bad Example:

if (days > 30) {
chargeLateFee()
}

Good Example:

const val MAX_DAYS_WITHOUT_LATE_FEE = 30

if (days > MAX_DAYS_WITHOUT_LATE_FEE) {
chargeLateFee()
}

Explanation: The constant MAX_DAYS_WITHOUT_LATE_FEE makes the code more readable and easier to maintain.

Error Handling

  • Handle errors gracefully, without obscuring the main logic of the code.
  • Use exceptions rather than returning error codes.
  • Ensure that your code fails early when something goes wrong, making debugging easier.

Bad Example:

function processOrder($order) {
if ($order == null) {
return -1; // error code
}
// process order
}

Good Example:

function processOrder($order) {
if ($order == null) {
throw new InvalidArgumentException("Order cannot be null");
}
// process order
}

Explanation: Using exceptions instead of error codes makes the code cleaner and easier to understand.

Keep Code Simple and Direct (KISS Principle)

  • Avoid complex solutions when a simpler one will do.
  • Write code that is easy to understand and straightforward, minimizing unnecessary complexity.

Bad Example:

fun getUserStatus(isActive: Boolean): String {
return if (isActive == true) {
"active"
} else {
"inactive"
}
}

Good Example:

fun getUserStatus(isActive: Boolean) = if (isActive) "active" else "inactive"

Explanation: The second example is much simpler and more direct.

Encapsulation

  • Hide implementation details and expose only what’s necessary.
  • Keep internal data and methods private to the class and provide a clear interface.

Bad Example:

class BankAccount {
var balance: Double = 0.0

fun deposit(amount: Double) {
balance += amount
}

fun withdraw(amount: Double) {
balance -= amount
}
}

Problem: The balance variable is public, so anyone can directly modify it, leading to potential issues.

Good Example:

class BankAccount {
private var balance: Double = 0.0

fun deposit(amount: Double) {
if (amount > 0) {
balance += amount
}
}

fun withdraw(amount: Double) {
if (amount > 0 && amount <= balance) {
balance -= amount
}
}

fun getBalance(): Double {
return balance
}
}

Explanation: In this version, balance is encapsulated (kept private), and only accessible via methods. This ensures that deposits and withdrawals follow specific rules, keeping the data safe from unwanted changes.

Avoid Side Effects

  • Functions should not change the state of variables or objects outside their scope, unless that’s their explicit purpose.
  • Functions with side effects can cause unexpected bugs and make the code harder to follow.

Bad Example:

class Order {
items: string[] = [];
status: string = 'pending';

addItem(item: string) {
this.items.push(item);
this.updateInventory(item); // Side effect: updating inventory within addItem
}

private updateInventory(item: string) {
// Logic to update inventory count
console.log(`Inventory updated for ${item}`);
}
}

Problem: The addItem method adds an item to the order and also updates the inventory. This side effect (updating inventory) is not obvious and can lead to unexpected behaviors, especially if someone calls addItem without knowing it affects the inventory.

Good Example:

class Order {
items: string[] = [];
status: string = 'pending';

addItem(item: string) {
this.items.push(item);
}
}

class InventoryManager {
updateInventory(item: string) {
// Logic to update inventory count
console.log(`Inventory updated for ${item}`);
}
}

// Usage
const order = new Order();
order.addItem('Margherita');

const inventoryManager = new InventoryManager();
inventoryManager.updateInventory('Margherita');

Explanation: In the good example, the addItem method is solely responsible for adding items to the order and does not trigger any other actions. The responsibility of updating the inventory is moved to a separate class, InventoryManager. This way, there is no hidden side effect when adding an item to the order, and the code becomes more modular and predictable.

Testing

  • Make sure your code is covered by unit tests, ensuring that changes don’t break existing functionality.
  • Write tests before writing code (if possible follow Test-Driven Development (TDD))

Bad Example:

fun calculateDiscount(price: Double): Double {
// logic to calculate discount
}

Good Example:

fun calculateDiscount(price: Double): Double {
return price * 0.9
}

// Tests written first
fun testCalculateDiscount() {
val price = 100.0
val discount = calculateDiscount(price)
assert(discount == 90.0)
}

Explanation: Tests are written first to guide the implementation.

Consistent Formatting

  • Keep the formatting of your code consistent — indentation, line breaks, and spacing should follow the same pattern.
  • Consistency makes it easier for the entire team to read and maintain the code.

Bad Example:

function calculateDiscount($price){if($price>100){return $price*0.9;}else{return $price*1.0;}}

Good Example:

function calculateDiscount($price) {
if ($price > 100) {
return $price * 0.9;
} else {
return $price * 1.0;
}
}

Explanation: The properly formatted version is easier to understand and maintain, with clear indentation.

Refactor Regularly

  • Clean up code as you go. Don’t wait for a “refactor later” stage.
  • Refactoring small sections of code over time is more manageable than large rewrites.

Bad Example (before refactoring):

fun process() {
if (condition1) {
// complex logic
}
if (condition2) {
// more complex logic
}
}

Good Example (after refactoring):

fun process() {
handleCondition1()
handleCondition2()
}

fun handleCondition1() {
if (condition1) {
// extracted logic
}
}

fun handleCondition2() {
if (condition2) {
// extracted logic
}
}

Explanation: The logic is split into smaller methods, making it easier to maintain and understand.

Conclusion

In conclusion, we’ve explored some of the key principles from Clean Code that are crucial for software engineers to write cleaner, more maintainable code. By applying these rules, you can improve code readability, reduce bugs, and make collaboration easier across teams.

Further reading

If you want to dive deeper into Clean Code, check out the Clean Code book by Robert C. Martin.

Here’s a great open-source project that applies Clean Code principles, feel free to explore it for more practical insights.

Try implementing one of the Clean Code principles in your next pull request and see how it impacts your workflow. I’d love to hear your experience!

--

--