ivankorobkov / python-inject

Python dependency injection
Apache License 2.0
700 stars 79 forks source link

Registering different implementations of the same interface. #108

Open shtlrs opened 2 months ago

shtlrs commented 2 months ago

I think the issue title says it all, but one issue i faced a lot is registering 2 or more different implementations of the same interface.

This can turn out to be useful for example when introducing abstractions in the form of chains of responsibility.

Just like middlewares work for example in web frameworks, you have multiple middlewares and then we iterate on them and each one handle the request per the same interface.

Also, it should be possible to make named registrations of instances of the same interface, so that when you need a particular instance that you define based on some key, you can get it from the container.

This is possible in frameworks like Autofac or SimpleInjector.

ivankorobkov commented 2 months ago

Hi.

but one issue i faced a lot is registering 2 or more different implementations of the same interface.

Inject is specifically designed to represent an application is a typed object graph. It simplifies reasoning about the whole application.

This can turn out to be useful for example when introducing abstractions in the form of chains of responsibility.

Please, take a look at the example in this comment https://github.com/ivankorobkov/python-inject/issues/104#issuecomment-2141168883

Also, it should be possible to make named registrations of instances of the same interface

In my opinion, it imitates different types in a custom way. It is usually better to just create different types for different use cases.

shtlrs commented 2 months ago

Hi 👋

Inject is specifically designed to represent an application is a typed object graph. It simplifies reasoning about the whole application.

I get the idea, but when we talk about typed object graph, does it imply that a node can have one child only ?

Please, take a look at the example in this comment https://github.com/ivankorobkov/python-inject/issues/104#issuecomment-2141168883

I did check that, but it's confusing to do that for different reasons:

In my opinion, it imitates different types in a custom way. It is usually better to just create different types for different use cases.

Sure, but i still believe it could use a type binding too. Like, I want this implementation of that interface, but now that I think of it, I am guessing this could be done with the params decorator.


class Interface(ABC):
    def method(self):
         pass

class ImplementationX(Interface):
    def method(self):
        pass

@injector.params(param=ImplementationX)
def somefunc(param: Interface)
    param.method()
    ...
ivankorobkov commented 2 months ago

does it imply that a node can have one child only

It does imply that a single type has a single implementation.

The fact that you don't configure the injector is confusing, at least IMO.

There is no need to configure a binding of MyType to MyType, inject can just instantiate it for you without any custom bindings.

Take a look at this case:

@injector.autoparams()
def somefunc(service: MyService)

You already specified that you need MyService. Inject can instantiate a singleton for you.

Like, I want this implementation of that interface, but now that I think of it, I am guessing this could be done with the params decorator.

@injector.params(param=ImplementationX)
def somefunc(param: Interface)

That's exactly not an implementation but another type:

@injector.autoparam()
def somefunc(param: InterfaceX)

And that's the idea. Just use the types to specify the whole application as a typed object graph.

shtlrs commented 2 months ago

There is no need to configure a binding of MyType to MyType, inject can just instantiate it for you without any custom bindings.

Yes, but it needs to be able to know how to instantiate it, not all constructors won't take params.

That's exactly not an implementation but another type: It does imply that a single type has a single implementation.

Aren't both of these contradictory ?

It's theoretically a type yes, but it's used as in implementation (I forgot the @abstractmethod)

ivankorobkov commented 2 months ago

Yes, but it needs to be able to know how to instantiate it, not all constructors won't take params.

Good point. In a typical application the only thing that needs to be provided is a configuration instance. All other params are part of an object graph. And the configuration can be bound manually.

Aren't both of these contradictory ?

It just means that a type is what others use and depend on, an an implementation is an actual hidden, private implementation of the type.

Just a random image from google:

Type is an external circle around a dot, which is an implementation. And the whole graph is an application.

shtlrs commented 2 months ago

Type is an external circle around a dot, which is an implementation. And the whole graph is an application.

Ok, I understand that.

However, what happens when the type needs to be polymorphic, and i need to use different implementation of it based on specific context ?

Meaning, if i have this application like this

image

How can implementations C and D use different implementations of the X type ? The idea is mainly to register two implementations A and B of type X, it doesn't make much sense to me to create a different type for it.

ivankorobkov commented 2 months ago

Simple example:

class Cache:
  pass

class MemCache(Cache):
  pass

class RedisCache(Cache):
  pass

# Later

class UserService:
  cache = inject.attr(Cache)

class MailService:
  cache = inject.attr(RedisCache) # not named cache "redis"

# Bindings

def configure(binder):
  binder.bind(Cache, MemCache())