KodrAus / rust-ioc

Playground for dependency injection in Rust
54 stars 3 forks source link

How to do automatic resolving? #8

Open zerkalica opened 7 years ago

zerkalica commented 7 years ago

In rust-ioc we need to resolve any dependency semi-manually:

    fn resolve((x, y): Self::Dependency) -> Self {
        Z {
            x: x.into_inner(),
            y: y.into_inner(),
        }
    }

In C# ninject we can bind interface to class


public class WarriorModule : NinjectModule
{
    public override void Load() 
    {
        this.Bind<IWeapon>().To<Sword>();
    }
}

We can use concrete types and automatically inject classes without binding.

Something like this:

class Sword {}

class Samurai 
{
    readonly Sword weapon;
    public Samurai(Sword weapon) 
    {
        this.weapon = weapon;
    }

    public void Attack(string target) 
    {
        this.weapon.Hit(target);
    }
}

Do you think about same style?

  1. It's possible to do same in rust traits?
  2. If not, how to use reflection, may be create some language extensions for it?
KodrAus commented 7 years ago

That's a good question! So currently resolution is provided by a trait that you implement, which needs a little manual work and doesn't support abstract dependencies, but does resolution at compile-time. That catches any cycles or 'missing' dependencies as early as possible.

From a resolution point of view, you could associate a factory method with a trait bound, and resolve that as a trait object. As for removing the manual implementation, the most idiomatic approach would probably be a custom derive macro, which is about as close as you get to reflection in Rust.

So it seems like all the key pieces are there, but I think there are a few things that could complicate it:

This is just a random dump of my thoughts at this stage :) This repo is just an experiment, so I don't expect anyone to actually use this code.

What do you think?

U007D commented 7 years ago

I think this is a very interesting experiment--thank you for posting not only the repo, but so much of your thinking. It's helpful to gain the benefits of your insight as I go down this same DI/IoC path.

KodrAus commented 7 years ago

No worries @U007D! If you're looking at experimenting in this space too I'd be interested to see where you end up :)

Lately I've been structuring components in my Rust apps a bit like this:

// A component
#[auto_impl(Fn)] // a little utility to automatically impl `GetProductQuery` for all `Fn(GetProduct) -> Result<GetProductResult, QueryError>`
pub trait GetProductQuery {
    fn get_product(&self, query: GetProduct) -> Result<GetProductResult, QueryError>;
}

// Dependency injection
pub fn get_product_query<TStore>(store: TStore) -> impl GetProductQuery
    where TStore: ProductStore
{
    // The actual implementation
    move |query: GetProduct| {
        let ProductData { id, title, .. } = store.get(query.id)?.ok_or("not found")?.into_data();

        Ok(GetProductResult {
            id: id,
            title: title
        })
    }
}

// Dependency resolution
impl Resolver {
    pub fn get_product_query(&self) -> impl GetProductQuery {
        let store = self.product_store();

        get_product_query(store)
    }
}

I've got a little sample app I'm working on at the moment that uses this approach. I plan to throw it up on GitHub this month. The essential bits are:

So you end up with something that separates the concerns of dependency injection, resolution and storage without a lot of magic.

I think there's definitely room for some kind of full-fledged IoC container for Rust that gives you even more flexibility to manage these things with a mix of compile-time and run-time work. Containers make figuring out how to compose your app together much more obvious.

U007D commented 7 years ago

Wow, that is elegant! Thank you for sharing your thinking! My colleague @bgbahoue and I have been thinking about DI off and on for a few months now. He's created a remarkably capable first pass at emulating C#'s AutoFac. You can see it at https://github.com/humanenginuity/shaku.

We also plan to open source this; meanwhile we're wrestling with how many attributes to get rid of; how much pain are we signing up for by heavily depending on procedural macros (unstable), the various limitations of Trait objects (eg. methods returning Self), generics and the inability to generate types at runtime, and so on.

I like how your concept minimizes magic, and subsequently the drama. Looking forward to seeing the sample app!