Dependency Inversion in Rust

I am trying to find a way to be able to parametrize a struct's functionality by using generics, at compile time.

Context:

My specific problem is that I have a struct which internally uses a function from an external crate.I would like to measure how many times this function is called. So I thought that I can create a mocked function, that maintains a counter, and then just passes the actual computation to the real function.

So I began thinking how I could use the dependency inversion principle to be able either pass the real function or a mocked version.

I know that for DI to work, I need to define an abstract interface, which probably means a trait. So I would also have to define two different types, one that simply calls the function, and one that maintains the counter.

Another "constrain" I have, is that I would like to not "pollute" my crate's API too much. By doing what I had in mind, I would need to add a generic parameter constrained on the trait, and this will be visible to the users. The users would also need to pass the "real implementation" type when constructing my struct. I could get away with this by defining some macro but I am not sure if this is idiomatic. Finally, I don't want to sacrifice the efficiency of the actual implementation for the sake of testing. So in principle the production version of my struct should be as fast as if it didn't use DI, so for example I don't want to use dynamic dispatch.

So to offer dependency injection indeed you'd use some polymorphism somewhere (thus, dyn Trait or generics), and if using generics, you'd want to hide that generic parameter down the line for the main API. But that doesn't mean you could have a more advanced API for this.

I'd thus recommend the following approach:

pub
struct YourTypeMockable<…, F : MockableApi> { … }

pub
type YourType<…> = YourTypeMockable<…, TheUnmockedImplementation>;

impl<…, F : MockableApi> YourTypeMockable<…, F> {
    // … methods but for the constructor

    pub /* or just pub(in crate) */
    fn new_mocked(constructor_args…, f: F) -> Self { … }
}

impl<…> YourType<…> {
    pub
    fn new(constructor_args…) -> Self {
        YourTypeMockable::new_mocked(
            constructor_args…,
            TheUnmockedImplementation::new(…),
        )
    }
}

That way, people would be able to do YourType::new(…) and all the methods that go with it, without needing to ever mention the default implementation, but the moment you'd want to mock it, you'd use the more advanced YourTypeMockable::new_mocked(…, MockedImplementor::new(…)).

5 Likes

Assigning a default for the F parameter is an alternative to writing a separate type alias:

pub
struct YourType<…, F = TheUnmockedImplementation> { … }

// F not specified, so we're talking about the default
impl<...> YourType<...> {
    pub
    fn new(constructor_args…) -> Self {
        Self::new_mocked(
            constructor_args…,
            TheUnmockedImplementation::new(…),
        )
    }
}

impl<…, F : MockableApi> YourType<…, F> {
    // … methods but for the constructor

    pub /* or just pub(in crate) */
    fn new_mocked(constructor_args…, f: F) -> Self { … }
}
2 Likes

Thank you both for the replies. This is more or less what I have done for the time being with one difference.

I initially also tried using a type alias, and this had the somewhat negative side effect that I had to expose TheUnmockedImplementation type as public, and also the internal "advanced" API. To me this seems a bit not so clean, but I am not sure about how rust deals with these things.

So for now I opted for the newtype pattern where i did something like this:

pub struct MyType<T>(MyTypeInternal<T, TheUnmockedImplementation>);

impl<T> MyType<T> {
    // add here the functions i want public and delegate them to self.0
}

While this doesn't expose any internals, it has quite a bit of boilerplate. Also, MyTypeInternal is also Deref, which means that I would have to re-implement Deref for MyType, and if I understand correctly this would add one more step of indirection, since 2 deref calls are needed. Since performance is more important in this case than a cleaner api, maybe I will move back to using a type alias instead.

thanks again!

By default I prefer to recommend the two explicit paths / type alias approach rather than default type parameters, since the latter often feature stuff that can be confusing for the caller (e.g., given a Foo<T=…>, the fact that Foo::new() and <Foo>::new() do not behave the same). That being said, the main annoyance is for the constructor, so for this example the drawback may be negated enough for the enhanced sugar to be worth it :slightly_smiling_face:

1 Like

If you want:

  • a nice API;
  • zero-cost-ly mockable (in the default impl case, at least);
  • and stuff that shouldn't be too cumbersome,

then, since the first two points would require boilerplate, you'd need to resort to macros to hide that boilerplate.

mod mockable {
    generateMyType! { F, for[F : 'static + Send + Sync + Fn(…) -> …] }
}
pub mod public {
    generateMyType! { TheUnmockedImplementation }
}
// where
macro_rules! generateMyType {(
    $Implementor:ty $(, for[$F:ident $($bounds:tt)*] )?
) => (
    pub
    struct MyType<T, $($F $($bounds)*)?> {
        foo: Foo<T>,
        …
        mockable_field: $Implementor,
    }

    impl<T, $($F $(bounds)*)?> MyType<T, $($F)?> {
        …
    }
)} use generateMyType;

That being said, if you are satisfied with the ergonomics of Deref, then if you mark the fn deref as #[inline], it really should be zero cost (Deref does not necessarily imply indirection, it can just be a pointer-tweaking operation (e.g., shifting the address by an offset, or thinning a pointer)). For instance, in the case of a struct Wrapper(Inner); pattern, where Deref would go from Wrapper to Inner, it will be a no-op on --release.

But if you deref to MyTypeInternal, if it is truly internal, downstream users will be unable to use that Deref.

2 Likes

Yes, macros did cross my mind, but unfortunately they are one area of rust that I really know too little. And since I was writting the code to learn something else, I didn't want to mix the two together. But Now I will have motivation to do a macro based project next.

As for deref, I don't deref to the internal MyTypeInternal type, but to what MyTypeInternal derefs to. To give a bit more of a context, I was writing a re-implementation of mutexes using the futex syscall in linux. So I have a type FutexGuard which is Deref and DerefMut, with the target being the locked type. So I now have a FutexGuardInternal which derefs to the locked value, and a struct FutexGuard<T>(FutexGuardInternal<T, TheUnmockedImplementation>) which derefs just by returning &*self.0. I guess it is possible that there is no indirection here, just playing with the pointer type. Maybe i should look at the LLVM IR, or the generated assembly. But to be honest, i benchmarked the code before and after the dependency inversion change and I didn't see a noticeable difference on the running time.

Thanks for all your detailed suggestions once again!

1 Like

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.