Lesson 3
Mastering Clean Code in C#: Functions and Methods
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 ProcessOrder method, which is manageable but has the potential to become unwieldy over time:

C#
1public void ProcessOrder(Order order, Inventory inventory, Logger logger) 2{ 3 // Step 1: Validate the order 4 if (!order.IsValid()) 5 { 6 logger.Log("Invalid Order"); 7 return; 8 } 9 10 // Step 2: Process payment 11 if (!order.ProcessPayment()) 12 { 13 logger.Log("Payment failed"); 14 return; 15 } 16 17 // Step 3: Update inventory 18 inventory.Update(order.Items); 19 20 // Step 4: Notify customer 21 order.NotifyCustomer(); 22 23 // Step 5: Log order processing 24 logger.Log("Order processed successfully"); 25}

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

C#
1public void ProcessOrder(Order order, Inventory inventory, Logger logger) 2{ 3 if (!ValidateOrder(order, logger)) return; 4 if (!ProcessPayment(order, logger)) return; 5 UpdateInventory(order, inventory); 6 NotifyCustomer(order); 7 LogOrderProcessing(logger); 8} 9 10private bool ValidateOrder(Order order, Logger logger) 11{ 12 if (!order.IsValid()) 13 { 14 logger.Log("Invalid Order"); 15 return false; 16 } 17 return true; 18} 19 20private bool ProcessPayment(Order order, Logger logger) 21{ 22 if (!order.ProcessPayment()) 23 { 24 logger.Log("Payment failed"); 25 return false; 26 } 27 return true; 28} 29 30private void UpdateInventory(Order order, Inventory inventory) 31{ 32 inventory.Update(order.Items); 33} 34 35private void NotifyCustomer(Order order) 36{ 37 order.NotifyCustomer(); 38} 39 40private void LogOrderProcessing(Logger logger) 41{ 42 logger.Log("Order processed successfully"); 43}
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 SaveAndNotifyUser method, which is both too lengthy and does multiple different things at once:

C#
1public void SaveAndNotifyUser(User user, DataSource dataSource, HttpClient httpClient) 2{ 3 // Save user to the database 4 string sql = "INSERT INTO users (name, email) VALUES (@name, @Email)"; 5 6 using (var connection = dataSource.GetConnection()) 7 using (var command = new SqlCommand(sql, connection)) 8 { 9 command.Parameters.AddWithValue("@Name", user.Name); 10 command.Parameters.AddWithValue("@Email", user.Email); 11 connection.Open(); 12 command.ExecuteNonQuery(); 13 } 14 15 // Send a welcome email to the user 16 var content = new StringContent(JsonConvert.SerializeObject(user), Encoding.UTF8, "application/json"); 17 var response = httpClient.PostAsync("/sendWelcomeEmail", content).Result; 18 if (!response.IsSuccessStatusCode) 19 { 20 throw new Exception("Failed to send email"); 21 } 22}

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

C#
1public void SaveAndNotifyUser(User user, DataSource dataSource, HttpClient httpClient) 2{ 3 SaveUser(user, dataSource); 4 NotifyUser(user, httpClient); 5} 6 7private void SaveUser(User user, DataSource dataSource) 8{ 9 string sql = "INSERT INTO users (name, email) VALUES (@name, @Email)"; 10 11 using (var connection = dataSource.GetConnection()) 12 using (var command = new SqlCommand(sql, connection)) 13 { 14 command.Parameters.AddWithValue("@Name", user.Name); 15 command.Parameters.AddWithValue("@Email", user.Email); 16 connection.Open(); 17 command.ExecuteNonQuery(); 18 } 19} 20 21private void NotifyUser(User user, HttpClient httpClient) 22{ 23 var content = new StringContent(JsonConvert.SerializeObject(user), Encoding.UTF8, "application/json"); 24 var response = httpClient.PostAsync("/sendWelcomeEmail", content).Result; 25 if (!response.IsSuccessStatusCode) 26 { 27 throw new Exception("Failed to send email"); 28 } 29}
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 SaveAddress method below with five arguments, which makes the function less clean:

C#
1public void SaveAddress(string street, string city, string state, string zipCode, string country) 2{ 3 // Logic to save address 4}

A cleaner version encapsulates the details into an Address object, reducing the number of arguments and making the method signature clearer:

C#
1public void SaveAddress(Address address) 2{ 3 // Logic to save address 4}
Avoid Boolean Flags

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

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

C#
1public void SetFlag(User user, bool isAdmin) 2{ 3 // Logic based on flag 4}

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

C#
1public void GrantAdminPrivileges(User user) 2{ 3 // Logic for admin rights 4} 5 6public void RevokeAdminPrivileges(User user) 7{ 8 // Logic to remove admin rights 9}
Avoid Side Effects

A side effect occurs when a method modifies some state outside its scope or relies on something external. This can lead to unpredictable behavior and reduce code reliability.

Below, the AddToTotal method demonstrates a side effect by modifying an external state:

C#
1// Not Clean - Side Effect 2public int AddToTotal(ref int total, int value) 3{ 4 total += value; // modifies external state 5 return total; 6}

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

C#
1// Clean - No Side Effect 🌟 2public int CalculateTotal(int initial, int value) 3{ 4 return initial + value; 5}
Don't Repeat Yourself (DRY)

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

The PrintUserInfo and PrintManagerInfo methods below repeat similar logic, violating the DRY principle:

C#
1public void PrintUserInfo(User user) 2{ 3 Console.WriteLine("Name: " + user.Name); 4 Console.WriteLine("Email: " + user.Email); 5} 6 7public void PrintManagerInfo(Manager manager) 8{ 9 Console.WriteLine("Name: " + manager.Name); 10 Console.WriteLine("Email: " + manager.Email); 11}

To adhere to DRY principles, use a generalized PrintInfo method. Ensure that both User and Manager inherit from a common Person base class or interface that includes Name and Email properties:

C#
1public void PrintInfo(Person person) 2{ 3 Console.WriteLine("Name: " + person.Name); 4 Console.WriteLine("Email: " + person.Email); 5}

This approach ensures that both User and Manager can be printed using the PrintInfo method without code duplication.

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.