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
UserServicewas used externally, forced us to alter the internal structure ofUserService. UserServiceis now aware of the fact thatUserDatabaseis used elsewhere. TheUserServiceonly needs theUserDatabaseto call 1 function. Why should it have to be aware of the fact that theUserDatabaseis 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.