Welcome to your next step in mastering Clean Code! 🚀 Previously, we emphasized the significance of naming conventions in clean coding. Now, we delve into the realm of functions and methods, which serve as the backbone of application logic and are crucial for code organization and execution. Structuring these functions effectively is vital for enhancing the clarity and maintainability of a codebase. In this lesson, we'll explore best practices and techniques to ensure our code remains clean, efficient, and readable.
Let's outline the key principles for writing clean functions:
- Keep functions small. Small functions are easier to read, comprehend, and maintain.
- Focus on a single task. A function dedicated to one task is more reliable and simpler to debug.
- Limit arguments to three or fewer. Excessive arguments complicate the function signature and make it difficult to understand and use.
- Avoid boolean flags. Boolean flags can obscure the code's purpose; consider separate methods for different behaviors.
- Eliminate side effects. Functions should avoid altering external states or depending on external changes to ensure predictability.
- Implement the DRY principle. Employ helper functions to reuse code, minimizing redundancy and enhancing maintainability.
Now, let's take a closer look at each of these rules.
Functions should remain small, and if they become too large, consider splitting them into multiple, focused functions. While there's no fixed rule on what counts as large, a common guideline is around 15 to 25 lines of code, often defined by team conventions.
Below, you can see the processOrder
function, which is manageable but has the potential to become unwieldy over time:
Kotlin1fun processOrder(order: Order, inventory: Inventory, logger: Logger) { 2 // Step 1: Validate the order 3 if (!order.isValid()) { 4 logger.log("Invalid Order") 5 return 6 } 7 8 // Step 2: Process payment 9 if (!order.processPayment()) { 10 logger.log("Payment failed") 11 return 12 } 13 14 // Step 3: Update inventory 15 inventory.update(order.items) 16 17 // Step 4: Notify customer 18 order.notifyCustomer() 19 20 // Step 5: Log order processing 21 logger.log("Order processed successfully") 22}
Given that this process involves multiple steps, it can be improved by extracting each step into a dedicated private function, as shown below:
Kotlin1fun processOrder(order: Order, inventory: Inventory, logger: Logger) { 2 if (!validateOrder(order, logger)) return 3 if (!processPayment(order, logger)) return 4 updateInventory(order, inventory) 5 notifyCustomer(order) 6 logOrderProcessing(logger) 7} 8 9private fun validateOrder(order: Order, logger: Logger): Boolean { 10 if (!order.isValid()) { 11 logger.log("Invalid Order") 12 return false 13 } 14 return true 15} 16 17private fun processPayment(order: Order, logger: Logger): Boolean { 18 if (!order.processPayment()) { 19 logger.log("Payment failed") 20 return false 21 } 22 return true 23} 24 25private fun updateInventory(order: Order, inventory: Inventory) { 26 inventory.update(order.items) 27} 28 29private fun notifyCustomer(order: Order) { 30 order.notifyCustomer() 31} 32 33private fun logOrderProcessing(logger: Logger) { 34 logger.log("Order processed successfully") 35}
A function should embody the principle of doing one thing only. If a function handles multiple responsibilities, it may include several logical sections. Below you can see the saveAndNotifyUser
function, which is both too lengthy and does multiple different things at once:
Kotlin1fun saveAndNotifyUser(user: User, dataSource: DataSource, webClient: WebClient) { 2 // Save user to the database 3 val sql = "INSERT INTO users (name, email) VALUES (?, ?)" 4 5 try { 6 dataSource.connection.use { connection -> 7 connection.prepareStatement(sql).use { statement -> 8 statement.setString(1, user.name) 9 statement.setString(2, user.email) 10 statement.executeUpdate() 11 } 12 } 13 } catch (e: SQLException) { 14 e.printStackTrace() // Handle SQL exception 15 } 16 17 // Send a welcome email to the user 18 webClient.post() 19 .uri("/sendWelcomeEmail") 20 .bodyValue(user) 21 .retrieve() 22 .onStatus(HttpStatus::isError) { response -> throw RuntimeException("Failed to send email") } 23 .block() 24}
To enhance this code, you can create two dedicated functions for saving the user and sending the welcome email. This results in dedicated responsibilities for each function and clearer code coordination:
Kotlin1fun saveAndNotifyUser(user: User, dataSource: DataSource, webClient: WebClient) { 2 saveUser(user, dataSource) 3 notifyUser(user, webClient) 4} 5 6private fun saveUser(user: User, dataSource: DataSource) { 7 val sql = "INSERT INTO users (name, email) VALUES (?, ?)" 8 9 try { 10 dataSource.connection.use { connection -> 11 connection.prepareStatement(sql).use { statement -> 12 statement.setString(1, user.name) 13 statement.setString(2, user.email) 14 statement.executeUpdate() 15 } 16 } 17 } catch (e: SQLException) { 18 e.printStackTrace() // Handle SQL exception 19 } 20} 21 22private fun notifyUser(user: User, webClient: WebClient) { 23 webClient.post() 24 .uri("/sendWelcomeEmail") 25 .bodyValue(user) 26 .retrieve() 27 .onStatus(HttpStatus::isError) { throw RuntimeException("Failed to send email") } 28 .block() 29}
A side effect occurs when a function modifies some state outside its scope or relies on something external. This can lead to unpredictable behavior and reduce code reliability.
Below, the addToTotal
function demonstrates a side effect by modifying an external state:
Kotlin1// Not Clean - Side Effect 2fun addToTotal(value: Int): Int { 3 total += value // Modifies external state 4 return total 5}
A cleaner version, calculateTotal
, performs the operation without altering any external state:
Kotlin1// Clean - No Side Effect 🌟 2fun calculateTotal(initial: Int, value: Int): Int { 3 return initial + value 4}
Avoid code repetition by introducing helper functions to reduce redundancy and improve maintainability.
The printUserInfo
and printManagerInfo
functions below repeat similar logic, violating the DRY principle:
Kotlin1fun printUserInfo(user: User) { 2 println("Name: ${user.name}") 3 println("Email: ${user.email}") 4} 5 6fun printManagerInfo(manager: Manager) { 7 println("Name: ${manager.name}") 8 println("Email: ${manager.email}") 9}
To adhere to DRY principles, use a generalized printInfo
function that operates on a parent Person
type:
Kotlin1fun printInfo(person: Person) { 2 println("Name: ${person.name}") 3 println("Email: ${person.email}") 4}
In this lesson, we learned that clean functions are key to maintaining readable and maintainable code. By keeping functions small, adhering to the Single Responsibility Principle, limiting arguments, avoiding side effects, and embracing the DRY principle, you set a strong foundation for clean coding. Next, we'll practice these principles to further sharpen your coding skills! 🎓