Module "globals" without being global

Hello all,

I'm wondering if someone can help with the following scenario.

I come from a C background, where the design pattern is somewhat flat. For certain things (such as a database connection), I might have this as a global in that it's declared as extern from a header file, which means whenever that header file is included, I have access to that database handle.

In Rust, I'm thinking about how I might achieve this from a similar scenario. I know I have the "option" (used in the loosest sense) if using something like: pub static DB: DBType = foo; but this feels incorrect and seems as though Rust is making you choose a different pattern, which I'm all for!

So... let's say I wrapped this DB example in a struct which holds some additional state:

struct DBState {
    connection: String,
    path:  String,
}

If I then implemented some fns on it:

impl DBState {
    pub fn new (...);
    pub fn connection(...);
}

At some point (presumably from fn main() although not necessarily so), I'm going to have to instantiate a DBState to setup a connection to the Database. That fine, and I can do that. But, if I have my code separated into different modules -- some of which might require access to the DBState's connection() -- how would I ensure that instantiated instance is available to those modules? As I see, it I have a few options:

  1. Somehow pass an instance of DBState to every function which seems counter-intuitive;
  2. Use a static variable in a module.
  3. Would using a trait help here?

Options 1 and 2 seem incorrect to me. Option 3 might work, but I'm not sure from a design perspective, how that influences the most straight-forward route.

I hope I've been clear -- and sorry if not, but if I can provide additional context, do please let me know.

Thanks in advance for everyone's thoughts!

1 Like

Option 1 is the normal way to do it in Rust. If you want a global (option 2), something like once_cell is the way to go. This lets you specify how the DBState should be initialized the first time it's accessed.

1 Like

Hey @cole-miller

Thank you very much for a speedy reply. I'll have a read of once_cell. I suppose my question is more around the design patterns which should be considered with Rust, when it was previously much "easier" (one could say "incorrect") in other languages to make this a global.

If option 1 is the preferred way, how does one deal with the fact that in order to propagate the DBStruct information around, there could be a point where that's passed into a function which ultimately does nothing with it.

Thanks!

I'm not sure I understand—if a function doesn't need to use the DBState then it shouldn't require a DBState to be passed in. Could you give a code example of what you mean?

For inspiration you could look at how this is handled in existing Rust libraries for interacting with a database, like rusqlite or postgres.

1 Like

Passing down the DBState is the right way to do it. This pattern is called dependency injection (however, please ignore dependency injection frameworks, which are usually overly complicated machinery to hide/automate passing of dependencies).

If you already have another object representing your application, then DBState could be a field on it.

struct MyApp {
 db: DBState,
}

and essentially implement everything that you want "global" as a method on MyApp, and it will have self.db accessible.

If you have multiple objects that may need to access the db, then use Arc<DBState>. This will let you reference it from any object anywhere, without fighting the borrow checker.

And in the simplest form, just make it a funciton argument.

fn save_to_db(db: &DBState, other_stuff: T)

and apply that recursively to any function that needs it. I know it sounds tedious and that's a lot of extra arguments. On the upside, it makes such functions stateless, so they're easier to test. And you immediately know which ones require the DB, and which ones don't.

Hi @kornel

Thank you very much for this. I take your point about not using a dependency injection framework. I've now structured things such that the dependency injection idea you suggest is now a part of the design which makes things much easier.

Thanks to everyone else who was so helpful in answering my question, I really appreciate it.

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.