I find myself using a "context pattern" often when developing in Rust. I am probably not the first thing to think of something like this, but I haven't seen anything like it on the web, at least for Rust specifically. So I thought I should share.
The pattern looks like (working definition):
- Define a
Context
struct with data used for the duration of the application - Define a
Context::init() -> Result<Self>
method to initialize the context. This mayErr
, for example, on invalid program arguments. - Define a
Context::run(&self)
method to run the program after context is initialized. - Define further implementation methods inside
impl Context
with&self
to access context. - Finally, define main as
fn main() -> Result { Context::init()?.run() }
And here is how it looks in code:
struct Context {
// program configuration, immutable
config: Config,
// other objects used for the duration of the application
// there could be anything here
client: HttpClient,
}
// example program config
struct Config {
username: String,
password: String,
url: String,
debug_mode: bool,
}
fn main() -> Result<()> {
Context::init()?.run()
}
impl Context {
fn init() -> Result<Self> {
// parse env::args or some other source to build Config, Err on invalid input
let config = build_config()?;
// initialize other global objects...
let client = build_client()?;
Ok(Self { config, client })
}
}
// helper methods for Context::init() can go here
// main program logic is in this impl Context
impl Context {
fn run(&self) -> Result<()> {
self.login()?;
self.fetch_data()?;
// etc...
Ok(())
}
fn login(&self) -> Result<()> {
// here I can use context like `self.config.username`
}
// etc...
}
I like this pattern because 1) function signatures are clean and 2) refactoring is made easy since I don't have to pass the same thing around to different functions as often.
An alternative is to use once_cell
or lazy_static!
for the global variables. But these don't work very well if you want to initialize with some error handling (using Result
).
If I need mutability for a field in Context
, I would probably use RefCell
or some interior mutability on that field.
There are some obvious pitfalls. You need to be careful not to over-extend the scope of an object by throwing it into the Context
for convenience. And, you wouldn't want this pattern to get out of hand with a complex program. The impl Context
should only hold the "top layer" of application logic. A context struct should be private to a single module.
You could repeat the pattern for a module/feature.
mod feature {
pub fn do_feature(inputs...) {
FeatureContext::init(inputs).run()
}
// private
struct FeatureContext { ... }
}
What are your thoughts? Would you adopt this pattern in your code? Why or why not? What would you do differently?