Project structure best practices

Hello

When creating an application in C# I use the n-tier architecture. This means a separate folder and namespace for the Domain (classes), UserInterface, Business Logic Layer, and Data Access Layer.
In C++ I create two folders src and headers, to separate my declaration and implementation.
In Python I write everything in one file and maybe separate my project in a few modules, each module holding the classes and functions belonging together.
In Java I use folders data, model and util,..,

These are considered best practices to organize your source code, but what is best practice to organize your code in Rust? I tried to copy the best practices from the other languages but that failed miserably.

I also don't understand modules, are this namespaces?
So I could put all my structs (classes) in a module named 'Domain'? Like this:

Person.rs:

    pub mod Domain {
        pub struct Person {
            name: str,
            age: u32,
        }
    }

Car.rs:

    pub mod Domain {
        pub struct Car {
            brand: str,
            amount_kilometers: u32,
        }
    }

Also for test-code, I just write my tests in the same source-file I want to test?? Shouldn't this be put in a separate test-folder?

Anyway, what are the best practices to create your project structure in Rust?

Thanks!

A Rust module defines both a namespace and a privacy boundary (like a class in C++).

Each module defines its own namespace, you can't split a namespace in multiple modules like in C++. One nice advantage of the way Rust does this is that synchronization of namespace name and module name is trivially guarantee. In this respect, Rust's modules are similar to Python's.

Because modules are Rust's privacy boundary, putting fine-grained unit tests in the same module is useful, as it allows you to access private struct members in tests without polluting your API with accessors that are meaningless to your target audience.

On their side, coarser-grained integration tests are best put in a separate "tests" directory, as 1/it avoids putting too much clutter in a single module and 2/it makes sure that you test API visibility and e.g. don't mistake pub(crate) with pub.

The Cargo guide has a nice description of standard project folder layout, you may find it useful if you want to learn more about how Rust supports things like multiple integration tests or multiple binaries in a single application.

1 Like

The way I structure items into modules is very organic. Usually I write a bunch of things in one file, and as I do I occasionally say to myself, "hey, 99% of the code I write here shouldn't need to see this private member. Let's guarantee that it can't". And so I take those things that do need to see it and put them in a nested module with use super::* (creating a privacy boundary without all the headache of making a new file and copying use statements).

pub use thing::Thing;
mod thing {
    use super::*;

    pub struct Thing {
        private_field: i8,
    }

    ... stuff ...
}

Sometimes one of these inner modules begins to grow large, or start having inner modules of its own with a lot of rightward drift. When that happens, I split it out into a new file, deleting the use super::* and replacing it with a proper list of imports.

If I later find myself wanting to split things out of that new file, I eventually turn it into a directory.

The resulting code structure basically depends on how the code evolved over time. It's highly nested, with trees of variable size, and it is "self-similar," in that any given directory in src at any depth was structured under similar principles, guided by privacy considerations.

In cases where something nested deeply ends up being useful to other parts of the crate, you have two options: make the thing and all of its parent modules pub(crate) (the lazy solution), or you can take this opportunity to clean it up, add more documentation if need be and move it up to somewhere nearer the root of the tree where more modules can see it. You can guess which of these solutions I prefer. :wink:

One disadvantage of this method is that things may be annoying to locate, but an IDE with Go to Definition makes this a non-issue.


There are a couple of things worth noting about standard directory names. cargo specifically recognizes top-level directories named tests and examples, as well as the directory src/bin. Anything in these directories are automatically associated with, you guessed it, tests (cargo test), examples (cargo run --example=xyz) and binaries (cargo run --bin=xyz) named after the files.


One final note: Here, I am talking about how to organize your crate internally. These are not the same considerations you should use when deciding how your public API is nested.

You'll notice how I used pub use child::Thing in my example above, so that the item still appears to be defined in the parent module. Generally speaking, I believe that most crates should expose a generally flat namespace heirarchy, to minimize the number of details a user needs to remember in order to use your crate. Only expose modules in a public API when you are confident that the user will benefit from the items being namespaced.

6 Likes

Thanks for your answers. I made a small test project and wrote everything in one file.
Is this the way to do it?

What I'm thinking:

  • Separating the modules in separate files
  • Making a folder for the modules having nested modules, and making each nested module a separate file in that folder

But how exactly would I do this?

Code (only consider the modules and structure, actual code not important for this question):
//Module holding all the structs, enumerations,...
pub mod domain {

    use std::string::ToString;

    pub struct Cat {
        name: String,
        age: u8,
        color: CatColor,
        race: CatRace,
    }
    impl Cat {
        pub fn new(name: String, age: u8, color: CatColor, race: CatRace) -> Cat {
            Cat {
                name: name,
                age: age,
                color: color,
                race: race,
            }
        }

        pub fn to_string(&self) -> String {
            let mut s : String = String::new();
            s = format!("Name: {name}\nAge: {age}\nColor: {color}\nRace: {race}",
                    name = self.name,
                    age = self.age,
                    color = self.color.to_string(),
                    race = self.race.to_string(),
                );

            s
        }
    }

    #[derive(Display, Debug)]
    pub enum CatColor {
        Black,
        White,
        Orange,
        Gray,
    }
    impl CatColor {
        pub fn from_u8(value: u8) -> CatColor {
            match value {
                1 => CatColor::Black,
                2 => CatColor::White,
                3 => CatColor::Orange,
                4 => CatColor::Gray,
                _ => {
                    eprintln!("Invalid CatColor selected!");
                    std::process::exit(0);
                }
            }
        }
        
    }

    #[derive(Display, Debug)]
    pub enum CatRace {
        Streetcat,
        AmericanShorthair,
        BritishShorthair,
        Bengal,
        EgyptianMau,
    }
    impl CatRace {
        pub fn from_u8(value: u8) -> CatRace {
            match value {
                1 => CatRace::Streetcat,
                2 => CatRace::AmericanShorthair,
                3 => CatRace::BritishShorthair,
                4 => CatRace::Bengal,
                5 => CatRace::EgyptianMau,
                _ => {
                    eprintln!("Invalid CatRace selected!");
                    std::process::exit(0);
                }
            }
        }
    }
}

//Module holding functions necessary for the userinterface
pub mod user_interface {

    pub mod display {
        use std::io;
        
        //Function to display the start menu
        pub fn display_start_menu() {
            println!("Welcome to the cat database!");
            println!("1) Add cat");
            println!("2) View all cats");
        }

        //Function to display add-cat-menu
        pub fn display_add_cat_menu() {
            println!("Give all the info about the cat!");
        }

        //Function to display view-all-cats-menu
        pub fn display_view_all_cats_menu() {
            println!("List of all the cats:");
        }

        //Function to flush std output
        pub fn flush() {
            io::Write::flush(&mut io::stdout());
        }
    }

    pub mod input {
        use std::io;
        use std::io::stdin;

        //Function to get user input
        pub fn get_u8() -> u8 {
            print!(">");
            //Flush display
            io::Write::flush(&mut io::stdout());

            //Get input and put it in mutable string
            let mut input = String::new();
            stdin()
                .read_line(&mut input)
                .expect("No valid input given...");

            //If parsing succesful return value, else quit program
            match input.trim().parse::<u8>() {
                Ok(i) => {
                    i
                }
                Err(..) => {
                    eprintln!("{} is not a valid number...", input);
                    std::process::exit(0);
                }
            }
        }
        
        //Function to get user input
        pub fn get_str() -> String {
            print!(">");
            //Flush display
            io::Write::flush(&mut io::stdout());

            //Get input and put it in mutable string
            let mut input = String::new();
            stdin()
                .read_line(&mut input)
                .expect("No valid input given...");
    
            input
        }
    }
}

//Module to access, read, and write data
pub mod data_layer {
    use std::vec::Vec;
    use crate::domain::*;

    //To manage data in memory
    pub struct MemoryData {
        cats: Vec<Cat>,
    }
    impl MemoryData {
        pub fn new() -> MemoryData {
            MemoryData { cats: Vec::new() }
        }
        pub fn add(&mut self, cat: Cat) -> bool {
            self.cats.push(cat);
            true
        }
        pub fn read(&mut self) -> &Vec<Cat> {
            &self.cats
        }
    }

    //*TO ADD:* Struct to manage data in text file and database
    //will follow
}

//Module holding logic and binding everything together
pub mod app {
    use crate::user_interface;
    use crate::data_layer;
    use crate::domain::*;

    //Run the application
    pub fn run(db: &mut data_layer::MemoryData) {
        user_interface::display::display_start_menu();
        let option: u8 = user_interface::input::get_u8();
        execute_option(option, db);
        
    }

    //Execute the selected option with corresponding function
    pub fn execute_option(option: u8, db: &mut data_layer::MemoryData) {
        match option {
            1 => {
               add_cat(db); 
            }
            2 => {
                view_all_cats(db);
            }
            _ => {
                eprintln!("Invalid option '{}' selected...", option);
            }
        }
    }

    //Function to add a cat
    pub fn add_cat(db: &mut data_layer::MemoryData) {
        //Displaying menu header
        user_interface::display::display_add_cat_menu();

        //Getting al the info
        println!("Name:");
        let name: String = user_interface::input::get_str();

        println!("Age:");
        let age: u8 = user_interface::input::get_u8();

        println!("Color:\n1) Black 2) White 3) Orange 4) Gray");
        let color: CatColor = CatColor::from_u8(user_interface::input::get_u8());
        
        println!("Race:\n1) Streetcat 2) American Shorthair 3) British Shorthair 4) Bengal 5) Egyptian Mau");
        let race: CatRace = CatRace::from_u8(user_interface::input::get_u8());

        //Create cat object
        let cat: Cat = Cat::new(name, age, color, race);

        //Save cat object
        db.add(cat);

        println!("Cat added...");
    }

    //Function to view all cats
    pub fn view_all_cats(db: &mut data_layer::MemoryData) {
        //Displaying menu header
        user_interface::display::display_view_all_cats_menu();
        //user_interface::display::flush();
        std::io::Write::flush(&mut std::io::stdout());

        //Reading, iterating over, and outputting all the cats
        for cat in db.read() {
            println!("{}", cat.to_string());
        }

    }
}

//Crates
extern crate strum;
#[macro_use]
extern crate strum_macros;

fn main() {
    //Initialize database
    let ref mut db = data_layer::MemoryData::new();

    while true {
        app::run(db);
    }
}

Thanks!

So, if you split this out, src/main.rs would look like:

pub mod domain;
pub mod user_interface;
pub mod data_layer;
pub mod app;

//Crates
extern crate strum;
#[macro_use]
extern crate strum_macros;

fn main() {
    ...
}

src/user_interface.rs (or in older compilers, you had to put it at src/user_interface/mod.rs when it's a directory) would look like this

//! Functions necessary for the userinterface
pub mod display;
pub mod input;

(the //! is an "inner doc-comment"; since it's at the top of the module, it documents the module)

and src/user_interface/display.rs would look like:

use std::io;
        
/// Display the start menu.
pub fn display_start_menu() {
    println!("Welcome to the cat database!");
    println!("1) Add cat");
    println!("2) View all cats");
}

/// Display the add-cat-menu.
pub fn display_add_cat_menu() {
    println!("Give all the info about the cat!");
}

/// Display the view-all-cats-menu.
pub fn display_view_all_cats_menu() {
    println!("List of all the cats:");
}

/// Flush std output.
pub fn flush() {
    io::Write::flush(&mut io::stdout());
}

(the /// are outer doc-comments; they document the items below them. You can view your crate's documentation by typing cargo doc --open)

2 Likes

Also,

Is this the way to do it?

I would say, in this case, actually: no, because it's pretty clear in this example that you knew how you wanted things to be laid out from the get-go. (also, everything is pub so nothing currently benefits from the privacy barriers). I did not mean to suggest that you should force yourself to put things in the same file when you feel they belong in separate files! :stuck_out_tongue:

(though I understand from your question that you might not have been sure how to make the new files; hopefully that question has been addressed now!)

My advice was specifically for how to deal with cases when you're not sure how you want to break something into groups. In that case, you look for the things that could be made private to most of the rest of the code, and group them together with the things that use them.

(and the only reason I start with an inner module rather than making a new file is so that introducing a module feels "cheap;" I don't want the tedium of copying use ... lists to stop me from introducing privacy barriers anywhere they could be useful)

2 Likes

For something small like this I'd keep everything in one file, but I wouldn't use any modules unless you wanted to break some portion of the code into a separate file, or possibly for tests, or if you had some unsafe code that you wanted to constrain. But even in those last two cases I'd probably put each module in a separate file.

Basically if a set of code is self contained enough to want privacy or a separate namespace, I'd expect it to be something that you'd like to look at by itself.

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.