Beginner Guidance on Dependency Injections and Globals

Hello everyone,

I'm a bit new to Rust and would like some advice on the best way to handle shared dependencies between functions.

I am writing a single-threaded application that needs to use some global state in different parts of the program. In particular, I am sharing an HTTP client and a HashMap that stores a mapping of hostnames to IP addresses and other hostnames.

My first instinct was to pass the dependencies around in Rc<T> as arguments, but this became harder to manage as I added more dependencies, requiring me to update all usages of each function or struct that used them. I did some research and found that I could possibly pass an Rc'd context struct and use RefCell<T> on the members. Another option was using some kind of dependency injection library with macros that would auto-wire things for me.

I work primarily in Go, where using traditional dependency injection (either runtime or codegen) is commonplace. I'm wondering what the "canonical" way of dealing with this problem is.

Thanks in advance!

my personal opinion is, don't be afraid of using many arguments. for me, a function with 6 to 8 parameters are completely fine, you can design the argument types carefully so they are easy to use, easy to read, and easy to understand.

and for the rare situation where the parameters of a function is out of hand, you can usually use the builder pattern to improve the API ergonomics.

in general, rust prefers passing explicit arguments rather than implicit/shared global states. another common anti-pattern in rust is to bundle smaller data into a large "context" type (often used as Self), e.g. in order to reduce the number of the arguments to pass around, but this would very likely force you to fight the borrow checker all the time.

granted, you can of course use global state as long it fits your use case, but it's common to "hide" the data access behind some high level API. this is commonly see in some form of "singleton" pattern, such as in the embedded rust ecosystem.

as for dependency injection, this term is more popular in OOP, while not that commonly used in rust. if I understand your problem description correctly, I think your problem can probably be solved using the "extractor" pattern, which is a very rust specific patten. it is the pattern you see in some popular rust libraries including axum and bevy.

for example, in axum, a endpoint handler is just a normal async function, but the magic is in the types of the arguments: your handler can have arbitrarily many of arguments (well, arbitrary up to the library defined limit), and that's where the extractor pattern comes into play. here's an example what handlers look like in axum:

async fn get_user_things(
    Path(user_id): Path<Uuid>,
    Query(pagination): Query<Pagination>,
    user_agent: Option<TypedHeader<UserAgent>>,
    //...
) {
    // ...
}

further readings:

3 Likes

Thank you for the detailed response. I've never seen anything like the extractor pattern before, I'll read up on the links you sent. For now, I'm sticking with the multiple Rc arguments system I'm using right now, and I suppose I'll figure out an alternative if it becomes way too hard to manage.

I don't use any kind of tool or codegen for this. What I usually do is make these things Arc<T> or Arc<Mutex<T>> (in your case, perhaps Rc<RefCell<T>>), and have each component of the program take its dependencies as constructor arguments or configuration.

If two functions have lots of shared dependencies, maybe they can be methods of a struct that holds the dependencies.

It's considered good style to hide the Arc inside a little struct, as it's basically an implementation detail. For example, reqwest::Client is an Arc inside.

1 Like

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.