Lesson 3
Clean Functions and Methods in Ruby
Introduction

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.

Clean Functions at a Glance

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 state 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.

Keep Functions Small

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 process_order method, which is manageable but has the potential to become unwieldy over time:

Ruby
1def process_order(order, inventory, logger) 2 # Step 1: Validate the order 3 unless order.valid? 4 logger.log('Invalid Order') 5 return 6 end 7 8 # Step 2: Process payment 9 unless order.process_payment 10 logger.log('Payment failed') 11 return 12 end 13 14 # Step 3: Update inventory 15 inventory.update(order.items) 16 17 # Step 4: Notify customer 18 order.notify_customer 19 20 # Step 5: Log order processing 21 logger.log('Order processed successfully') 22end

Given that this process involves multiple steps, it can be improved by extracting each step into a dedicated private method, as shown below:

Ruby
1def process_order(order, inventory, logger) 2 return unless validate_order(order, logger) 3 return unless process_payment(order, logger) 4 5 update_inventory(order, inventory) 6 notify_customer(order) 7 log_order_processing(logger) 8end 9 10private 11 12def validate_order(order, logger) 13 if order.valid? 14 true 15 else 16 logger.log('Invalid Order') 17 false 18 end 19end 20 21def process_payment(order, logger) 22 if order.process_payment 23 true 24 else 25 logger.log('Payment failed') 26 false 27 end 28end 29 30def update_inventory(order, inventory) 31 inventory.update(order.items) 32end 33 34def notify_customer(order) 35 order.notify_customer 36end 37 38def log_order_processing(logger) 39 logger.log('Order processed successfully') 40end
Single Responsibility

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 save_and_notify_user method, which is both too lengthy and does multiple different things at once:

Ruby
1def save_and_notify_user(user, data_source, web_client) 2 # Save user to the database 3 sql = "INSERT INTO users (name, email) VALUES (?, ?)" 4 5 data_source.get_connection do |connection| 6 statement = connection.prepare(sql) 7 statement.execute(user.name, user.email) 8 end 9 10 # Send a welcome email to the user 11 response = web_client.post('/sendWelcomeEmail', user) 12 raise 'Failed to send email' unless response.successful? 13end

To enhance this code, you can create two dedicated methods for saving the user and sending the welcome email, resulting in dedicated responsibilities for each method and clearer code coordination:

Ruby
1def save_and_notify_user(user, data_source, web_client) 2 save_user(user, data_source) 3 notify_user(user, web_client) 4end 5 6private 7 8def save_user(user, data_source) 9 sql = "INSERT INTO users (name, email) VALUES (?, ?)" 10 11 data_source.get_connection do |connection| 12 statement = connection.prepare(sql) 13 statement.execute(user.name, user.email) 14 end 15end 16 17def notify_user(user, web_client) 18 response = web_client.post('/sendWelcomeEmail', user) 19 raise 'Failed to send email' unless response.successful? 20end
Limit Number of Arguments

Try to keep the number of function arguments to a maximum of three, as having too many can make functions less understandable and harder to use effectively. 🤔

Consider the save_address method below with five arguments, which makes the function less clean:

Ruby
1def save_address(street, city, state, zip_code, country) 2 # Logic to save address 3end

A cleaner version encapsulates the details into a hash, reducing the number of arguments and making the method signature clearer:

Ruby
1def save_address(address) 2 # Logic to save address using address hash 3end
Avoid Boolean Flags

Boolean flags in functions can create confusion, as they often suggest multiple pathways or behaviors within a single function. Instead, use separate methods for distinct behaviors. 🚫

The set_flag method below uses a boolean flag to indicate user status, leading to potential complexity:

Ruby
1def set_flag(user, is_admin) 2 # Logic based on flag 3end

A cleaner approach is to have distinct methods representing different behaviors:

Ruby
1def grant_admin_privileges(user) 2 # Logic for admin rights 3end 4 5def revoke_admin_privileges(user) 6 # Logic to remove admin rights 7end
Avoid Side Effects

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 add_to_total method demonstrates a side effect by modifying an external state:

Ruby
1# Not Clean - Side Effect 2def add_to_total(value) 3 @total += value # modifies external state 4 @total 5end

A cleaner version, calculate_total, performs the operation without altering any external state:

Ruby
1# Clean - No Side Effect 🌟 2def calculate_total(initial, value) 3 initial + value 4end
Don't Repeat Yourself (DRY)

Avoid code repetition by introducing helper methods to reduce redundancy and improve maintainability.

The print_user_info and print_manager_info methods below repeat similar logic, violating the DRY principle. Here, user and manager are instances of classes with the attributes name and email:

Ruby
1def print_user_info(user) 2 puts "Name: #{user.name}" 3 puts "Email: #{user.email}" 4end 5 6def print_manager_info(manager) 7 puts "Name: #{manager.name}" 8 puts "Email: #{manager.email}" 9end

To adhere to the DRY principles, use a generalized print_info method that operates on a parent Person type. Here, user, manager, and person are instances of classes that share common attributes name and email, allowing them to be used interchangeably in this context:

Ruby
1def print_info(person) 2 puts "Name: #{person.name}" 3 puts "Email: #{person.email}" 4end
Summary

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! 🎓

Enjoy this lesson? Now it's time to practice with Cosmo!
Practice is how you turn knowledge into actual skills.