Rust Refactoring to enhance Modularity

Rust Refactoring to enhance Modularity
Introduction

Producing reliable system software is challenging. Pointer manipulation, mutable heap data, and concurrency are typically employed to realize high performance, but cause subtle bugs that are notoriously difficult to uncover and reproduce. The Rust programing language resolves this problem by preventing some errors statically through its type system, which associates an exclusive capability with each mutable memory location. At each time, any exclusive capability is held by at the most one executing function: only that code may access the memory location. These exclusive capabilities are often exchanged for shared capabilities, with which many references can read a location, but none can modify it when aliasing is desired. The type system of Rust applies this discipline, making ensure that well-typed Rust programs are bound to not exhibit data races, have dangling pointers or unexpected side effects through aliased references.

Description

We’ll fix four issues that need to do with the program’s structure to improve our program. First of all, our key function now performs two tasks: it parses arguments and reads files. For such little function, this isn’t a serious problem. Though, if we still grow our program inside main, the amount of separate tasks the most function handles would grow. As a function gains responsibilities, it becomes harder to reason about, harder to check , and harder to vary without breaking one among its parts. It’s best to separate functionality so each function is liable for one task.
This problem also ties into the second issue: although query and filename are configuration variables to our program, variables like contents are wont to perform the program’s logic. The longer main becomes, the more variables we’ll got to bring into scope; the more variables we’ve in scope, the harder it’ll be to stay track of the aim of every . It’s best to group the configuration variables into one structure to form their purpose clear.
The third problem is that we’ve used expect to print a mistake message when reading the file fails, but the error message just prints Something went wrong reading the file. Reading a file can fail during a number of ways: for instance , the file might be missing, or we’d not have permission to open it. Right now, no matter things , we’d print the Something went wrong reading the file error message, which wouldn’t give the user any information!
At fourth, we use expect repeatedly to manipulate different errors, and if the user runs our program without specifying enough arguments, they’ll get an index out of bounds error from Rust that doesn’t clearly explain the matter . It may be best if all the error-handling code were in one place so future maintainers had just one place to consult within the code if the error-handling logic needed to vary . Happening whole the error-handling code in one place also will make sure that we’re printing messages which will be meaningful to our end users.

Rust Refactoring to enhance Modularity
Let’s come to address these four issues by refactoring our project.

Separation of Concerns for Binary Projects

The organizational problem of allocating responsibility for multiple tasks to the most function is common to several binary projects. Consequently, the Rust community has developed a process to use as a suggestion for splitting the separate concerns of a binary program when main starts getting large. the method has the subsequent steps:

Distribute the program into a main.rs and a lib.rs and move your program’s logic to lib.rs.
As long as the instruction parsing logic is little , it can remain in main.rs. Extract it from main.rs and move it to lib.rs when the instruction parsing logic starts getting complicated, . The responsibilities that remain within the main function after this process should be limited to the following:

  • Calling the instruction parsing logic with the argument values
  • Setting up the other configuration
  • Calling a run function in lib.rs
  • Handling the error if run returns a mistake

This way is about separating concerns: main.rs handles running the program, and lib.rs handles all the logic of the task at hand. This structure enable us to test all of our program’s logic by moving it into functions in lib.rs. the sole code that is still in main.rs are going to be sufficiently small to verify its correctness by reading it as we can’t test the most function directly, . Let’s re-do our program by following this process.

Extracting the Argument Parser

We’ll extract the functionality for parsing arguments into a function that main will call to organize for moving the instruction parsing logic to src/lib.rs. Listing 12-5 shows the new start of main that calls a replacement function parse_config, which we’ll define in src/main.rs for the instant .

Filename: src/main.rs
fn main() {
let args: Vec = env::args().collect();

let (query, filename) = parse_config(&args);

// --snip--
}
fn parse_config(args: &[String]) -> (&str, &str) {
let query = &args[1];
let filename = &args[2];
(query, filename)
}

We’re still collecting the instruction arguments into a vector, but rather than assigning the argument value at index 1 to the variable query and therefore the argument value at index 2 to the variable filename within the most function, we pass the entire vector to the parse_config function. We still create the query and filename variables in main, but main not has the responsibility of determining how the instruction arguments and variables correspond.
This re-done work could seem like overkill for our small program, but we’re refactoring in small, incremental steps. After making this alteration , run the program again to verify that the argument parsing still works. It’s good to see your progress often, to assist identify the explanation for problems once they occur.

Grouping Configuration Values

We can take another small step to enhance the parse_config function further. At the instant , we’re returning a tuple, on the other hand we immediately break that tuple into individual parts again. this is often a symbol that perhaps we don’t have the proper abstraction yet.

One another indicator that shows there’s room for improvement is that the config a part of parse_config, which means that the 2 values we return are related and are both a part of one configuration value. We’re not currently conveying this meaning within the structure of the info aside from by grouping the 2 values into a tuple; we could put the 2 values into one struct and provides each of the struct fields a meaningful name. Doing so will make it easier for future maintainers of this code to know how the various values relate to every other and what their purpose is.

Note: Using primitive values when a posh type would be more appropriate is an anti-pattern referred to as primitive obsession.

Filename: src/main.rs
fn main() {
let args: Vec = env::args().collect();
let config = parse_config(&args);
println!("Searching for {}", config.query);
println!("In file {}", config.filename);
let contents = fs::read_to_string(config.filename)
.expect("Something went wrong reading the file");
// --snip--
}
struct Config {
query: String,
filename: String,
}
fn parse_config(args: &[String]) -> Config {
let query = args[1].clone();
let filename = args[2].clone();
Config { query, filename }
}

 

We’ve added a struct named Config defined to possess fields named query and filename. The signature of parse_config now identify that it returns a Config value. We now define Config to contain owned String values within the body of parse_config, where we wont to return string slices that reference String values in args. The args variable in main is that the owner of the argument values and is merely letting the parse_config function borrow them, which suggests we’d violate Rust’s borrowing rules if Config tried to require ownership of the values in args.

We could manage the String data during a number of various ways, but the simplest , though somewhat inefficient, route is to call the clone method on the values. this may make a full copy of the info for the Config instance to have , which takes longer and memory than storing a regard to the string data. However, cloning the info also makes our code very straightforward because we don’t need to manage the lifetimes of the references; during this circumstance, abandoning a touch performance to realize simplicity may be a worthwhile trade-off.

Rust Refactoring to enhance Modularity

The Trade-Offs of Using clone

There’s a bent among many Rust aceans to avoid using clone to repair ownership problems due to its runtime cost. It’s better to possess a working program that’s a touch inefficient than to undertake to hyper optimize code on  first pass. As we become experienced with Rust, it’ll be easier to start out with the foremost efficient solution, except for now, it’s perfectly acceptable to call clone.

Our code now more precisely conveys that question and filename are related which their purpose is to configure how the program will work. Any code that uses these values knows to seek out them within the config instance within the fields named for his or her purpose.

Creating a Constructor for Config

So far, we’ve extracted the logic liable for parsing the instruction arguments from main and placed it within the parse_config function. Doing so helped us to ascertain that the query and filename values were related which relationship should be conveyed in our code. We then added a Config struct to call the related purpose of query and filename and to be ready to return the values’ names as struct field names from the parse_config function.

So now that the aim of the parse_config function is to make a Config instance, we will change parse_config from a clear function to a function named new that’s related to the Config struct. Making this alteration will make the code more idiomatic. we will create instances of types within the standard library, like String, by calling String::new. Similarly, by changing parse_config into a replacement function related to Config, we’ll be ready to create instances of Config by calling Config::new.

Filename: src/main.rs
fn main() {
let args: Vec = env::args().collect();
let config = Config::new(&args);
// --snip--
}
// --snip--
impl Config {
fn new(args: &[String]) -> Config {
let query = args[1].clone();
let filename = args[2].clone();

Config { query, filename }
}
}

We’ve revised and updated main where we were calling parse_config to instead call Config::new. We’ve converted the name of parse_config to new and moved it within an impl block, which associates the new function with Config. Try compiling this code again to form sure it works.

Leave a Comment