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 number?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?get 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?in 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!
A 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 codes 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.
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 the 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:
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 enables?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?is 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 the 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 does?not?have 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 is?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 provide?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 each?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 identifies that it returns a Config value. We now define Config to contain owned String values within the?body of parse_config, where we?want 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?in 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?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.
The Trade-Offs of Using the 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 the 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 to?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.
For more details visit:https://www.technologiesinindustry4.com/2021/07/rust-refactoring-to-enhance-modularity.html