In this article, we'll explore the process of refactoring a simple program that calculates the area of a rectangle using single variables, then gradually transitioning to using structs for better clarity and additional functionality. The example program is written in Rust, a systems programming language known for its emphasis on performance, memory safety, and zero-cost abstractions.
Initial Implementation with Single Variables
Let's start with the initial version of the program in Rust, where the area of a rectangle is calculated using individual variables for width and height.
Rust
// Filename: src/main.rs
fn main() {
let width1 = 30;
let height1 = 50;
println!(
"The area of the rectangle is {} square pixels.",
area(width1, height1)
);
}
fn area(width: u32, height: u32) -> u32 {
width * height
}
When this program is executed using `cargo run`, it successfully computes and prints the area of the rectangle, but there is room for improvement in terms of code clarity.
Refactoring with Tuples
To enhance the code's readability, we initially refactor it using tuples to group the width and height together.
Rust
// Filename: src/main.rs
fn main() {
let rect1 = (30, 50);
println!(
"The area of the rectangle is {} square pixels.",
area(rect1)
);
}
fn area(dimensions: (u32, u32)) -> u32 {
dimensions.0 * dimensions.1
}
While this version reduces the number of function parameters, it introduces a new challenge. Tuples don't name their elements, requiring us to index into the tuple, potentially leading to confusion.
Refactoring with Structs: Adding Meaning
To address the issues with tuples, we further refactor the code by introducing a struct named `Rectangle` with named fields for width and height.
Rust
// Filename: src/main.rs
struct Rectangle {
width: u32,
height: u32,
}
fn main() {
let rect1 = Rectangle {
width: 30,
height: 50,
};
println!(
"The area of the rectangle is {} square pixels.",
area(&rect1)
);
}
fn area(rectangle: &Rectangle) -> u32 {
rectangle.width * rectangle.height
}
Now, the code is more expressive, providing clear names for the width and height values and indicating their relationship within the context of a rectangle.
Adding Useful Functionality with Derived Traits
While refactoring, we encounter a situation where attempting to print an instance of `Rectangle` using `println!` results in an error. To address this, we opt in to implement the `Debug` trait for `Rectangle` using the `#[derive(Debug)]` attribute.
Rust
// Filename: src/main.rs
#[derive(Debug)]
struct Rectangle {
width: u32,
height: u32,
}
fn main() {
let rect1 = Rectangle {
width: 30,
height: 50,
};
println!("rect1 is {:?}", rect1);
}
Now, we can print the `Rectangle` instance for debugging purposes. The `#[derive(Debug)]` attribute generates the necessary code to enable debugging output.