audunhalland / entrait

Loosely coupled Rust application design made easy
83 stars 1 forks source link

Multiple implementations of the same trait? #6

Closed eckyputrady closed 2 years ago

eckyputrady commented 2 years ago

Hi,

I wonder if there's a way to have multiple implementations of the same trait?

Something like:

fn login(deps: &impl UserRepository, username: &str) {
   deps.user_exists(&username)
} 

fn postgres_user_exist(pool: &PgPool, username: &str) {
   // do pg stuff
}

fn inmemory_user_exist(data: &Map, username: &str) {
  // check in memory
}

And in the main.rs I would like to switch between postgres and in-memory based on, say, a config variable.

Thanks! :slightly_smiling_face:

audunhalland commented 2 years ago

Hmm, interesting! I think #[entrait] Trait {} can be used for this. I managed to write the following thing, which works:

use entrait::*;

#[entrait(Login)]
fn login(deps: &impl UserRepository, username: &str) {
    deps.user_exists(&username)
}

#[entrait]
trait UserRepository {
    fn user_exists(&self, username: &str);
}

struct PgApp;
struct InMemoryApp;

impl UserRepository for PgApp {
    fn user_exists(&self, username: &str) {}
}

impl UserRepository for InMemoryApp {
    fn user_exists(&self, username: &str) {}
}

fn test_mocked() {
    login(&unimock::mock(None), "username");
}

fn test_real() {
    // login(&Impl::new(PgApp), "username");
    // login(&Impl::new(InMemoryApp), "username");

    Impl::new(PgApp).login("username");
    Impl::new(InMemoryApp).login("username");
}

edit: Changed the Impl examples to call the Login trait method instead of the function

audunhalland commented 2 years ago

A problem with my first suggestion is that all of the application code will be duplicated (monomorphized) for PgApp and InMemoryApp. If you want to use a dyn solution instead, maybe something like this works:

use entrait::*;

#[entrait(Login)]
fn login(deps: &impl GetUserRepository, username: &str) {
    deps.get_user_repository().user_exists(username);
}

#[entrait(GetUserRepository)]
fn get_user_repository(app: &App) -> &dyn UserRepository {
    app.user_repository.as_ref()
}

#[entrait]
pub trait UserRepository {
    fn user_exists(&self, username: &str);
}

struct App {
    user_repository: Box<dyn UserRepository + Sync>,
}

struct PgUserRepository {}
impl UserRepository for PgUserRepository {
    fn user_exists(&self, username: &str) {}
}

fn test_real() {
    let app = Impl::new(App {
        user_repository: Box::new(PgUserRepository {}),
    });

    app.login("username");
}

edit: Well, the problem with this is that now it's not as easy to mock calls to UserRepository, because those methods are not direct dependencies any more. 🤔

audunhalland commented 2 years ago

I'm thinking of a feature that might simplify the scenario of dynamic leaf dependencies.

There's this trait:

trait UserRepository: 'static {
    fn user_exists(&self, username: &str) -> bool;
}

Using the #[entrait] attribute on this trait will generate an implementation for Impl<T> that requires that T also implements the trait. But that's not always what's wanted. In any case, we want to delegate som call from Impl<T> into some type that contains the real implementation. But going through T: Trait is evidently not always the best strategy.

So I'm thinking about a way to delegate by dynamically borrowing the implementation:

impl<T> UserRepository for Impl<T>
where
    T: Borrow<dyn UserRepository> + 'static,
{
    fn user_exists(&self, username: &str) {
        self.as_ref().borrow().user_exists(username);
    }
}

This implementation could be generated by e.g. #[entrait(dyn_borrow)]:

#[entrait(dyn_borrow)]
trait UserRepository: 'static {
    fn user_exists(&self, username: &str) -> bool;
}

The way to make it work for some application (this code must be written manually):

struct App {
    user_repository: Box<dyn UserRepository + Sync>,
}

impl std::borrow::Borrow<dyn UserRepository> for App {
    fn borrow(&self) -> &dyn UserRepository {
        self.user_repository.as_ref()
    }
}

struct PgUserRepository;

impl UserRepository for PgUserRepository {
    fn user_exists(&self, username: &str) {}
}
audunhalland commented 2 years ago

Implemented in https://github.com/audunhalland/entrait/commit/784a1249f3077a586465c9c2655e82a0fac9c201, I hope this is sufficient for your use case!

audunhalland commented 2 years ago

Released version 0.4.2 with this feature. Closing bug. Feel free to reopen if the new feature is still not adequate enough for your use case!

eckyputrady commented 2 years ago

This is awesome. Thanks @audunhalland !

audunhalland commented 2 years ago

See also https://github.com/audunhalland/entrait/issues/8 for an idea of how this could be improved on even more. I.e. not require things like this to be "leaf dependencies" - and to be able to do real dependency inversion. Which would mean that you can define a trait in an upstream crate ("inner layer"), implement it in an intermediate layer, and link things together at the main crate layer.