Skip to content

Instantly share code, notes, and snippets.

@Enforcer
Created April 4, 2021 18:51
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save Enforcer/920ecebdabb5c4b75c67bc43aac5b325 to your computer and use it in GitHub Desktop.
Save Enforcer/920ecebdabb5c4b75c67bc43aac5b325 to your computer and use it in GitHub Desktop.
# tested on Python3.9 with just injector installed (pip install injector==0.18.4)
from dataclasses import dataclass
from typing import TypeVar, Generic
from injector import Injector, Module, provider
TCommand = TypeVar("TCommand")
class Handler(Generic[TCommand]):
def __call__(self, command: TCommand) -> None:
raise NotImplementedError
@dataclass(frozen=True)
class Enrol:
student_id: int
course_id: int
class EnrolHandler(Handler[Enrol]):
def __call__(self, command: Enrol) -> None:
print(f"command: {command}")
class Enrolment(Module):
@provider
def enrol_handler(self) -> Handler[Enrol]:
return EnrolHandler()
class CommandBus:
def __init__(self, container: Injector) -> None:
self._container = container
def handle(self, command: TCommand) -> None:
command_cls: Type[TCommand] = type(command)
handler = self._container.get(Handler[command_cls])
handler(command)
container = Injector([Enrolment()], auto_bind=False)
command_bus = CommandBus(container)
command_bus.handle(Enrol(student_id=123000, course_id=666))
@vryazanov
Copy link

My bad, I meant Service Locator of course. Which container do you pass to constructor of CommandBus in this case? Do you have a separate container having only BaseHandler classes?

Besides that your examples are quite close / or almost the same what I'm doing in my code.

@vryazanov
Copy link

and if CommandBus uses global container inside it becomes a super object which in runtime can get any other object it wants, I guess this is what Service Locator pattern is about.

@Enforcer
Copy link
Author

No, Service Locator is about passing and using global container all around in various places and layers in code. That's actually the antipattern that is very easy to get with inject as it relies on a global state.

I disagree CommandBus becomes super object - container is kept in a private field (_container) which should discourage people from using it from the outside. And CommandBus itself won't be using the container to get any object - that's why these Handler generics are for. You can't just pass anything to CommandBus.handle method and expect it to get from container something it shouldn't

What would be the scenario when CommandBus gets something undesirable from the container?

@luru-eb
Copy link

luru-eb commented Feb 1, 2022

Hi @Enforcer

Very nice and clean implementation. I have one question:

How do you deal with dependencies in handlers? For example:

class EnrolHandler(Handler[Enrol]):
    def __init__(self, someService: SomeService) -> None:
        self._someService = someService

    def __call__(self, command: Enrol) -> None:
        self._someService.do(command.student_id)

Cheers!

@Enforcer
Copy link
Author

Enforcer commented Feb 1, 2022

Hi @luru-eb
one needs to tell container how to build EnrolHandler; it's still injected as you can see here https://gist.github.com/Enforcer/920ecebdabb5c4b75c67bc43aac5b325#file-injector_command_bus-py-L42

You just need to extend Injector's module:

class Enrolment(Module):
    @provider
    def some_service(self) -> SomeService:
        return SomeService()

    @provider
    def enrol_handler(self, some_service: SomeService) -> Handler[Enrol]:
        return EnrolHandler(some_service=some_service)

@luru-eb
Copy link

luru-eb commented Feb 1, 2022

Thank you so much! 🙌

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment