pcah/python-clean-architecture

DI: Container - register by interface

jakos opened this issue · 4 comments

jakos commented

We had a conversation about the difference between registering by name and registering by interface. I passed some motivations, stating that:

  • when you place a interface into inject object (ie. use annotation), both your IDE and static code analyzer know what fields & methods it has (great!),
  • there are times when you don't have possibility to provide an interface that all implementations inherit from (with factory methods in particular); then you can use registering by name,

And now I have the ultimate reason to differ between interface & name registration. Consider following example (exaggerated a bit, but only a tiny):

class ISession:

    @property
    def id(self): ...
    def save_data(self, key, data): ...
    def get_data(self, key): ...

class RedisSession:
    ...  # implementation

class ISessionWithUser(ISession):
    user: entities.User

class UserRedisSession(ISessionWithUser):
    ...  # implementation

# registering sub-interface
container.register_by_interface(ISessionWithUser, UserRedisSession)
# ... allows finding them with their parent interface
container.find_by_interface(ISession)
# >>> returns UserRedisSession instance even though its registration
# interface was more specific than the one given at the lookup call

Three notes:

  • do you want to implement this use case in this issue or is it enough for you?
  • here I see some benefit using a decorator to indicate that something is an interface for something: you do not have to look every time in the runtime iff queried interface has some subclass which provides a constructor
  • we already have an util to find all subclasses of a class (pca.utils.inspect.get_all_subclasses), so it isn't too hard to implement
  • if getting all sub-interfaces sounds time-consuming, you could use registration time (ie. application startup time; as then exist all super-interfaces of registered interface).

Eventually, described feature is just an application of Liskov Substitution Principle to our Dependency Injection solution.

Tell me what do you think about this.

Originally posted by @lhaze in #19 (comment)

lhaze commented

Food for thoughts:
I've noticed lately I have a few use-cases of the register by interface feature more. There will be cases when I would want to request a DI object based on a generic interface, not only a concrete interface. Generic in the meaning of typing, i.e.

class MyInteractor:
    input: InputPort[MyCommand] = Inject()

so the interface of input not only is some InputPort interface, but a specific InputPort[MyCommand]. This may mean that there's a

container.register_by_interface(InputPort, MyInputPort)

constructor, that takes MyCommand as an argument. How to place this use-case within our design?

I wasn't sure where to put this question so I chose what seemed to be a relevant enough issue! But let me know if you want me to move this question somewhere else.

Uncle Bob describes the "wiring" of dependencies as a responsibility of "main" (application). If "main" belongs to the outer circles of the clean architecture, and use cases (interactors) belong to an inner circle, shouldn't it be disallowed to have MyInteractor know about the concept of Inject()? You seem to be creating a dependency from an inner circle (the interactor) to an outer one (the di container) which breaks the dependency rule. Would love to know what you folks think about this use. Did I misread the example code?

lhaze commented

@jakos, a note after our refinement:
The issue here is to split.

  • Task (a): registering super-types of the interface
    • up to object or sth custom?
    • what to do with conflicting interfaces?
    • ... or should the container recognize only those types who are "designed" or @marked to serve as DI interfaces?
  • Task (b): what to do with generic types as interfaces? Two examples:
    • list[MyEntity]
    • InputPort[MyCommand]
lhaze commented

@ms-lolo

Hello! It's always nice to hear a good question. I'm sorry you'd had to wait so long to get a response. Let me explain why. Lately, only I was working on the library as the other colleagues are all involved with the main project we do and their private lives. Which is completely understandable. Additionally, the passing year was not so productive, as I was experiencing time of grief and sorrow in my family life. Long story short: I expect to be progressively more active here from now on, and if you want to see some more actions here, you are welcome to help.

I've moved the topic to the Discussion section.