Lesson 1
Applying Clean Code Principles in Rust: Understanding and Implementing the DRY Principle
Introduction

Welcome to the very first lesson of the "Applying Clean Code Principles in Rust" course! In this lesson, we will focus on a fundamental concept in clean coding: the DRY ("Don't Repeat Yourself") principle. Understanding DRY is crucial for writing efficient, maintainable, and clean code. This principle is vital for everyday software development, aiding in creating robust and easily modifiable codebases. Today, let's delve into the issues caused by repetitive code and explore strategies to combat redundancy using Rust. 🚀

Understanding the Problem

Repetitive functionality in code can introduce several issues that affect the efficiency and maintainability of your software:

  • Code Bloat: Repeating similar code across different parts of your application unnecessarily increases the size of the codebase. This makes the code harder to navigate and increases the chances of introducing errors.

  • Risk of Inconsistencies: When similar pieces of logic are scattered across different areas, it's easy for them to become out of sync during updates or bug fixes. This can result in logic discrepancies and potentially introduce new problems.

  • Maintenance Challenges: Updating code often requires modifications in multiple places, leading to increased work and a higher likelihood of errors. Redundant code makes it difficult for developers to ensure all necessary changes have been made consistently.

DRY Strategies

To adhere to the DRY principle and avoid repeating yourself, several strategies can be employed with Rust:

  • Extracting Methods: Move repeated logic into dedicated functions or methods within an impl block that can be reused. This promotes code reuse and simplifies updates while respecting Rust's ownership model and borrowing rules.

  • Extracting Variables: Consolidate repeated expressions or values into variables. Leveraging Rust's default immutability with let, we centralize changes and reduce potential errors.

  • Replacing Temporaries with Queries: Utilize functions or methods to compute values on demand, enhancing readability and reducing redundancy.

  • Using Generics and Traits: Abstract over common behaviors using generics and traits, reducing code repetition while leveraging Rust’s powerful type system.

  • Leveraging Macros: Employ macros to generate repetitive code patterns at compile time, ensuring DRY code without sacrificing performance.

  • Organizing Code with Modules and Crates: Structure your code into modules and crates to promote reuse and reduce duplication across larger projects.

Now, let's explore some practical examples in Rust.

Problem 1: Duplicated Business Logic

Consider the following problematic code snippet, where repetitive logic is used for calculating the total price based on different shipping methods:

Rust
1struct Item { 2 price: f64, 3 quantity: u32, 4} 5 6struct Order { 7 items: Vec<Item>, 8 tax: f64, 9} 10 11fn calculate_click_and_collect_total(order: &Order) -> f64 { 12 let items_total: f64 = order 13 .items 14 .iter() 15 .map(|item| item.price * f64::from(item.quantity)) 16 .sum(); 17 let shipping_cost = if items_total > 100.0 { 18 0.0 19 } else { 20 5.0 21 }; 22 items_total + shipping_cost + order.tax 23} 24 25fn calculate_post_shipment_total(order: &Order, is_express: bool) -> f64 { 26 let items_total: f64 = order 27 .items 28 .iter() 29 .map(|item| item.price * f64::from(item.quantity)) 30 .sum(); 31 let shipping_cost = if is_express { 32 items_total * 0.1 33 } else { 34 items_total * 0.05 35 }; 36 items_total + shipping_cost + order.tax 37}

Both functions contain duplicated logic for calculating the total price of items, making them error-prone and hard to maintain. Now, let's refactor this code.

Solution 1: Centralizing Logic by Extracting Methods

By consolidating the shared logic into a separate method, we can eliminate redundancy and streamline updates:

Rust
1struct Item { 2 price: f64, 3 quantity: u32, 4} 5 6struct Order { 7 items: Vec<Item>, 8 tax: f64, 9} 10 11impl Order { 12 fn calculate_items_total(&self) -> f64 { 13 self.items 14 .iter() 15 .map(|item| item.price * f64::from(item.quantity)) 16 .sum() 17 } 18} 19 20fn calculate_click_and_collect_total(order: &Order) -> f64 { 21 let items_total = order.calculate_items_total(); 22 let shipping_cost = if items_total > 100.0 { 0.0 } else { 5.0 }; 23 items_total + shipping_cost + order.tax 24} 25 26fn calculate_post_shipment_total(order: &Order, is_express: bool) -> f64 { 27 let items_total = order.calculate_items_total(); 28 let shipping_cost = if is_express { 29 items_total * 0.1 30 } else { 31 items_total * 0.05 32 }; 33 items_total + shipping_cost + order.tax 34}

By extracting the calculate_items_total method into the Order struct, we centralize the logic of item total calculation, leading to cleaner and more maintainable code. This method also respects Rust's ownership and borrowing rules by taking &self and returning a f64, avoiding unnecessary data copying.

Problem 2: Scattered Constants and Calculations

Let's look at an example that deals with repeated calculations for discount rates:

Rust
1fn apply_discount(price: f64, loyalty_level: f64) -> f64 { 2 let mut loyalty_discount = loyalty_level * 0.02; 3 let mut discounted_price = price * (1.0 - loyalty_discount); 4 5 let seasonal_discount = 0.10; 6 discounted_price *= (1.0 - seasonal_discount); 7 discounted_price 8}

Here, the discount rates are scattered throughout the code, which complicates management and updates. Additionally, variables are declared as mut unnecessarily, which goes against Rust's preference for immutability.

Solution 2: Organizing Constants and Calculations by Extracting Variables

We can simplify this by extracting the discount rates into immutable variables:

Rust
1fn apply_discount(price: f64, loyalty_level: f64) -> f64 { 2 let loyalty_discount = loyalty_level * 0.02; 3 let seasonal_discount = 0.10; 4 5 let discounted_price = price * (1.0 - loyalty_discount); 6 let final_price = discounted_price * (1.0 - seasonal_discount); 7 final_price 8}

In this refactored code:

  • Variables are declared as immutable (let), aligning with Rust's emphasis on immutability, which also reduces potential side effects and enhances code safety.
  • The calculation logic is more concise, improving readability and making it easier to understand the purpose of each variable.
  • Centralizing discount rates into variables facilitates easier updates, as changes are localized, reducing the risk of errors.

This code is cleaner, more readable, and allows changes in just one place. 🎉

Problem 3: Repeated Temporary Variables

Our final example involves temporary variables that lead to repetition:

Rust
1use std::time::{SystemTime, Duration}; 2 3fn is_eligible_for_discount(sign_up_date: SystemTime, purchase_history_size: usize) -> bool { 4 let twelve_weeks = Duration::from_secs(12 * 7 * 24 * 60 * 60); // Duration of 12 weeks 5 let new_customer = match SystemTime::now().duration_since(sign_up_date) { 6 Ok(duration) => duration < twelve_weeks, 7 Err(_) => false, 8 }; 9 new_customer && purchase_history_size > 5 10} 11 12fn is_eligible_for_loyalty_program(sign_up_date: SystemTime, loyalty_level: i32) -> bool { 13 let twelve_weeks = Duration::from_secs(12 * 7 * 24 * 60 * 60); 14 let new_customer = match SystemTime::now().duration_since(sign_up_date) { 15 Ok(duration) => duration < twelve_weeks, 16 Err(_) => false, 17 }; 18 new_customer || loyalty_level > 3 19}

The variable new_customer is used in multiple places, causing duplicated logic; this suggests that the code can be improved by refactoring.

As a side note, we're utilizing Rust's standard library std::time module for time calculations. The SystemTime type represents a moment in time, and Duration allows us to specify time intervals. We create a duration of twelve weeks using Duration::from_secs(12 * 7 * 24 * 60 * 60), and we compare times using SystemTime::now().duration_since(). This method returns a Result because the sign-up date could theoretically be in the future, which we handle appropriately in our code.

Solution 3: Using Functions for Computed Values

Let's refactor by extracting the logic into a function, reducing duplication and enhancing modularity:

Rust
1use std::time::{SystemTime, Duration}; 2 3fn is_new_customer(sign_up_date: SystemTime) -> bool { 4 let twelve_weeks = Duration::from_secs(12 * 7 * 24 * 60 * 60); // Duration of 12 weeks 5 match SystemTime::now().duration_since(sign_up_date) { 6 Ok(duration) => duration < twelve_weeks, 7 Err(_) => false, 8 } 9} 10 11fn is_eligible_for_discount(sign_up_date: SystemTime, purchase_history_size: usize) -> bool { 12 is_new_customer(sign_up_date) && purchase_history_size > 5 13} 14 15fn is_eligible_for_loyalty_program(sign_up_date: SystemTime, loyalty_level: i32) -> bool { 16 is_new_customer(sign_up_date) || loyalty_level > 3 17}

By creating the is_new_customer function, we've simplified the code and made it more maintainable. This function encapsulates the logic for determining if a customer is new based on their sign-up date and the current date.

Rust-Specific Considerations for DRY Code

When refactoring code in Rust to adhere to the DRY principle, it's important to consider Rust's ownership and borrowing rules. Functions should often accept references (&T) instead of taking ownership (T) to avoid unnecessary data copying and to work seamlessly with Rust's memory safety guarantees.

Additionally, Rust's emphasis on immutability encourages developers to write functions without side effects, leading to cleaner and more maintainable code. Utilizing Rust's features like pattern matching, enums, and error handling with Result and Option types can also help reduce code repetition by handling different cases elegantly.

Summary and Preparation for Practice

In this lesson, you learned about the DRY principle and strategies like Extracting Methods, Extracting Variables, and Replacing Temporaries with Queries using Rust. These strategies help create code that is easier to maintain, enhance, and understand by taking advantage of Rust's unique features like ownership, borrowing, and immutability.

By adhering to Rust's idioms—such as using explicit type conversions, preferring immutability, and following consistent coding styles—you can write code that not only avoids repetition but is also robust and idiomatic.

Next, you'll have the opportunity to apply these concepts in practical exercises, strengthening your ability to refactor code and uphold clean coding standards in Rust. Happy coding! 😊

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