Lesson 1
Understanding Code Smells in Rust
Introduction

Welcome to the very first lesson of the "Clean Code with Multiple Structs and Traits in Rust" course! ๐ŸŽ‰ This course aims to guide you in writing code that's easy to understand, maintain, and enhance. Within the broader scope of clean coding, effective struct collaboration is crucial for building well-structured applications. In this lesson, we will delve into the intricacies of struct collaboration and coupling โ€” key factors that can make or break the maintainability of your software. Specifically, we'll address some common "code smells" that indicate problems in struct interactions and explore ways to resolve them.

Overview of Struct Collaboration Challenges

Let's explore the challenges of struct collaboration by focusing on four common code smells:

  • Feature Envy: This occurs when a method in one struct is overly interested in methods or data in another struct.
  • Inappropriate Intimacy: Describes a situation where two structs are too closely intertwined, sharing private details.
  • Message Chains: Refer to sequences of method calls across several instances, indicating a lack of clear abstraction.
  • Middle Man: Exists when a struct primarily delegates its behavior to another struct without adding functionality.

Understanding these code smells will enable you to improve your struct designs, resulting in cleaner and more maintainable code.

Problems Arising During Struct Collaboration

These code smells can significantly impact system design and maintainability. Let's consider their implications:

  • They can lead to tightly coupled structs, making them difficult to modify or extend.
  • Code readability decreases, as it becomes unclear which struct is responsible for which functionality.

Addressing these issues often results in code that is not only easier to read but also more flexible and scalable. Tackling these problems can substantially improve software architecture, making it more robust and adaptable.

Feature Envy

Feature Envy occurs when a method in one struct is more interested in the fields or methods of another struct than its own. Here's an example:

Rust
1struct Item { 2 price: f64, 3 quantity: f64, 4} 5 6struct ShoppingCart { 7 items: Vec<Item>, 8} 9 10impl ShoppingCart { 11 fn new(items: Vec<Item>) -> ShoppingCart { 12 ShoppingCart { items } 13 } 14 15 fn calculate_total_price(&self) -> f64 { 16 self.items.iter().map(|item| item.price * item.quantity).sum() 17 } 18}

In this scenario, calculate_total_price in ShoppingCart overly accesses data from Item, indicating feature envy.

To refactor, consider moving the logic to the Item struct:

Rust
1struct Item { 2 price: f64, 3 quantity: f64, 4} 5 6impl Item { 7 fn calculate_total(&self) -> f64 { 8 self.price * self.quantity 9 } 10} 11 12struct ShoppingCart { 13 items: Vec<Item>, 14} 15 16impl ShoppingCart { 17 fn new(items: Vec<Item>) -> ShoppingCart { 18 ShoppingCart { items } 19 } 20 21 fn calculate_total_price(&self) -> f64 { 22 self.items.iter().map(Item::calculate_total).sum() 23 } 24}

Now, each Item calculates its own total, reducing dependency and distributing responsibility appropriately. โœ”๏ธ

Inappropriate Intimacy

Inappropriate Intimacy occurs when a struct is overly dependent on the internal details of another struct. Here's an example:

Rust
1struct Book { 2 title: String, 3 author: String, 4} 5 6struct Library { 7 book: Book, 8} 9 10impl Library { 11 fn print_book_details(&self) { 12 println!("Title: {}", self.book.title); 13 println!("Author: {}", self.book.author); 14 } 15}

In this scenario, the Library struct directly accesses the title and author fields of Book, tightly coupling Library to the internal representation of Book. This demonstrates inappropriate intimacy because any changes to Book's internal structure would require corresponding changes in Library.

To refactor, allow the Book struct to handle its own representation:

Rust
1struct Book { 2 title: String, 3 author: String, 4} 5 6impl Book { 7 fn details(&self) -> String { 8 format!("Title: {}\nAuthor: {}", self.title, self.author) 9 } 10} 11 12struct Library { 13 book: Book, 14} 15 16impl Library { 17 fn print_book_details(&self) { 18 println!("{}", self.book.details()); 19 } 20}

By moving the responsibility of presenting its details into the Book struct, we reduce the coupling between Library and Book. Now, Library doesn't need to know about the internal fields of Book, promoting better encapsulation and separation of concerns. ๐Ÿ›ก๏ธ

Message Chains

Message Chains occur when structs need to traverse multiple objects to access the methods they require. Here's a demonstration:

Rust
1struct User { 2 pub address: Address, 3} 4 5struct Address { 6 pub zip_code: ZipCode, 7} 8 9struct ZipCode; 10 11impl ZipCode { 12 pub fn postal_code(&self) -> &str { 13 "90210" 14 } 15} 16 17// Usage 18let user = User { address: Address { zip_code: ZipCode } }; 19user.address.zip_code.postal_code();

The chain user.address.zip_code.postal_code() illustrates this problem.

To simplify, encapsulate the access within methods:

Rust
1struct User { 2 address: Address, 3} 4 5impl User { 6 fn postal_code(&self) -> &str { 7 self.address.postal_code() 8 } 9} 10 11struct Address { 12 zip_code: ZipCode, 13} 14 15impl Address { 16 fn postal_code(&self) -> &str { 17 self.zip_code.postal_code() 18 } 19} 20 21struct ZipCode; 22 23impl ZipCode { 24 fn postal_code(&self) -> &str { 25 "90210" 26 } 27} 28 29// Usage 30let user = User { address: Address { zip_code: ZipCode } }; 31user.postal_code();

This adjustment makes the User struct responsible for retrieving its postal code, creating a clearer and more direct interface. ๐Ÿ“ฌ

Middle Man

A Middle Man problem arises when a struct does little else than delegate method calls to another struct, without adding significant value or functionality. This extra layer can make the codebase more complex without providing real benefits.

Here's an example illustrating this code smell:

Rust
1struct Data { 2 content: String, 3} 4 5enum Status { 6 Success, 7 Failure, 8} 9 10struct Handler { 11 processor: Processor, 12} 13 14impl Handler { 15 fn process_data(&self, data: &Data) { 16 self.processor.process_data(data); 17 } 18 19 fn get_status(&self) -> Status { 20 self.processor.get_status() 21 } 22} 23 24struct Processor; 25 26impl Processor { 27 fn process_data(&self, data: &Data) { 28 // Logic to process data 29 println!("Processing data: {:?}", data); 30 } 31 32 fn get_status(&self) -> Status { 33 // Return some status 34 Status::Success 35 } 36}

In this example, the Handler struct is simply forwarding method calls to Processor without adding any additional logic or abstraction. This makes Handler an unnecessary middle man, as it doesn't contribute any meaningful functionality beyond delegation.

To refactor, you can eliminate the middle man by using Processor directly:

Rust
1struct Data { 2 content: String, 3} 4 5enum Status { 6 Success, 7 Failure, 8} 9 10struct Processor; 11 12impl Processor { 13 fn process_data(&self, data: &Data) { 14 // Logic to process data 15 println!("Processing data: {:?}", data); 16 } 17 18 fn get_status(&self) -> Status { 19 // Return some status 20 Status::Success 21 } 22} 23 24// Usage 25let processor = Processor; 26let data = Data { content: String::from("Sample data") }; 27processor.process_data(&data); 28let status = processor.get_status(); 29println!("Processing status: {:?}", status);

By removing the unnecessary Handler struct (the middle man), the code becomes more straightforward and maintainable. Direct interaction with Processor eliminates the extra delegation layer, enhancing code clarity and reducing redundancy. ๐Ÿ”ฅ

Summary and Practice Heads-Up

In this lesson, you've explored several code smells associated with suboptimal struct collaboration and coupling, including Feature Envy, Inappropriate Intimacy, Message Chains, and Middle Man. By identifying and refactoring these smells, you can enhance your code's clarity and maintainability.

Get ready to apply these concepts with upcoming exercises, where you'll identify and refactor code smells using Rustโ€™s struct and trait systems, strengthening your skills. Keep striving for cleaner, more effective code! ๐ŸŒŸ

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