Abstracting reference containers (Rc, Arc, Box, ...)

Viewed 369

I've been hobby programming in Rust for a while now and there's something annoying me when I try to create abstractions. Here's a small example I made of the kind of code I end up with when writing a program in a dependency injection style:

type UserId = u8;

pub struct User {}

pub struct UserDatabase {}

impl UserDatabase {
    pub fn get(&self, user_id: UserId) -> User {
        todo!()
    }
}

pub struct UserService {
    user_database: UserDatabase
}

impl UserService {
    fn get_user(&self, user_id: UserId) -> User {
        self.user_database.get(user_id)
    }
}

fn main() {
    let user_database = UserDatabase{};

    let user_service = UserService{
        user_database
    };

    user_service.get_user(todo!());
}

In this example the UserService exposes a get_user method. Internally, this calls a UserDatabase. This UserDatabase is injected into the UserService in the main method.

So this all works well, but consider the scenario now that this UserDatabase needs to get used (injected into) another service:

pub struct SomeOtherService {
    user_database: UserDatabase
}

fn main() {
    let user_database = UserDatabase{};

    let user_service = UserService{
        user_database
    };

    let some_other_service = SomeOtherService{
        user_database
    };

    user_service.get_user(todo!());
}

Obviously this code doesn't compile as we've tried to move user_database twice in main.

So we have to wrap it in something like an Rc:

let user_database = Rc::new(UserDatabase{});

But this means now, in order to be able to inject it into user_service, we have to also wrap the type in the UserService struct in an Rc:

pub struct UserService {
    user_database: Rc<UserDatabase>
}

This bothers me because:

  • Changing the way the UserService was used externally, forced us to alter the internal structure of UserService.
  • UserService is now aware of the fact that UserDatabase is used elsewhere. The UserService only needs the UserDatabase to call 1 function. Why should it have to be aware of the fact that the UserDatabase is referenced elsewhere? Objectively I know the answer to this: it's because the program has to work out how to safely dereference the value from the heap without interfering with other parts of the code; but as programmers we don't want to leak unnecessary details to the implementation of a function.

Similarly for something like Arc, all the parts of the program would now have to be aware they are operating in a multi-threaded scenario.

Is there some way we can achieve a kind of structure where UserService is not aware of how the UserDatabase type is wrapped? One idea that comes to mind is having a trait:

trait GetUserDatabase {
    fn get(&self, user_id: UserId) -> User;
}

and then somehow being able to make Rc<UserDatabase> or Arc<UserDatabase> implement that trait.

Perhaps I'm missing some neat tricks here or perhaps I just need alter my traditional Golang/Java mindset when approaching patterns like dependency injection.

3 Answers

Rust is not designed for Java's dependency injection mindset, indeed.

UserService is now aware of the fact that UserDatabase is used elsewhere. The UserService only needs the UserDatabase to call 1 function. Why should it have to be aware of the fact that the UserDatabase is referenced elsewhere?

Because this is exactly the kind of things Rust wants to be explicit about.

This is not just a limitation: Rust wants you to know who owns your objects. The ownership system should design your mind and your program. This "share-style" is indeed not very Rusty - and one of the reasons Rc and friends are not commonly seen in idiomatic Rust (they do appear, but not at the same amount as GC'd languages).

Do not think about services - think about data. Instead of asking who needs access to the user database, ask of whom it is. The owner does not inject the DB to whoever needs it, he just let them take a look. If main() owns the database, the services should take a reference to it (or not exist at all). If both services own it, it should be Rc. Either way, you have to be explicit about that. And that's a good thing!

The existing answer is correct for your given situation, so you should definitely follow the advice given there and accept it as correct. This answer is intended to answer the direct question given in the title, for anyone else who happens to land here from a web search and does need this functionality.


The standard library trait you are looking for here is Borrow<T>, which means "you can borrow a T from this."

Borrow<T> is implemented for the smart pointers Rc<T>, Arc<T>, Box<T, _>, and Cow<'_, T>. Borrow<T> is even implemented for T and &T, so you can handle owned values and references exactly the same way as smart pointers.


One way to make use of this trait would be with generics:

pub struct UserService<UserDB: Borrow<UserDatabase>> {
    user_database: UserDB
}

The caveat to this approach is that UserService is now a generic type, so anything else that comes into contact with it needs to be aware (on some level) of the generic type argument. If you use this approach on every service type in the program, the generics may "infect" the rest of the program to a, quite frankly, obnoxious degree -- anything that used to accept a UserService now needs to accept a UserService<impl Borrow<UserDatabase>>. This also leaks details about the dependencies of a service to any facility that uses it, which is likely undesirable.


A second approach that doesn't have the downsides of the generic approach is:

pub struct UserService {
    user_database: Box<dyn Borrow<UserDatabase>>
}

Now if you have a UserDatabase, you can give it a Box<UserDatabase>. If you have an Rc<UserDatabase> you can give it a Box<Rc<UserDatabase>>. To borrow an existing instance without using a smart pointer, you can give it a Box<&UserDatabase>.

However, this approach has its own problems: there is an extra level of indirection, as well as requiring dynamic dispatch to invoke Borrow::borrow(). The performance hit could be worth the added flexibility, but that is a call you would have to make.

If you do dependency injection, you should likely base your design on a trait. For example:

pub trait UserDatabase {
    fn get(&self, user_id: UserId) -> User;
}

// UserService works with any database that implements the trait

pub struct UserService<Db> {
    user_database: Db,
}

impl<Db: UserDatabase> UserService<Db> {
    fn get_user(&self, user_id: UserId) -> User {
        self.user_database.get(user_id)
    }
}

The implementation of the trait is no more complex than before:

pub struct UserDatabaseImpl {}

impl UserDatabase for UserDatabaseImpl {
    fn get(&self, user_id: UserId) -> User {
        todo!()
    }
}

...and your main() looks the same, too:

fn main() {
    let user_database = UserDatabaseImpl {};
    let user_service = UserService { user_database };
    user_service.get_user(todo!());
}

But now, if you want to share the database, you no longer need to modify UserService - you only need to provide a new implementation of the trait that allows such sharing. In the above case it can even make use of the non-sharing implementation:

impl UserDatabase for Rc<UserDatabaseImpl> {
    fn get(&self, user_id: UserId) -> User {
        self.as_ref().get(user_id)
    }
}

The database can now be shared among multiple services without modifying the implementation of UserService or SomeOtherService:

pub struct SomeOtherService<Db> {
    user_database: Db,
}

fn main() {
    let user_database = Rc::new(UserDatabaseImpl {});
    let user_service = UserService {
        user_database: Rc::clone(&user_database),
    };
    let some_other_service = SomeOtherService {
        user_database: Rc::clone(&user_database),
    };
    user_service.get_user(todo!());
}
Related