Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

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.

like image 995
DazKins Avatar asked Sep 06 '25 03:09

DazKins


2 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!

like image 71
Chayim Friedman Avatar answered Sep 07 '25 20:09

Chayim Friedman


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!());
}
like image 44
user4815162342 Avatar answered Sep 07 '25 19:09

user4815162342