|# 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")|
|def __call__(self, command: TCommand) -> None:|
|def __call__(self, command: Enrol) -> None:|
|def enrol_handler(self) -> Handler[Enrol]:|
|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])|
|container = Injector([Enrolment()], auto_bind=False)|
|command_bus = CommandBus(container)|
Care to elaborate on why do you think so?
The container is a private detail of
What you propose would force developers to modify the creation logic of
Besides, what about constructing a handler? It's not a function. It's an instance of a class that has its own dependencies etc. Container deals with building it. With it being passed, we either have to have it build in advance (not practical) or use plain functions (limiting).
What are in your opinion the benefits of other implementation? What's wrong with this one?
You said this is a nice and easy way to extend a list of commands, but you use DI container like a service allocator and pass it to constructor directly. This is basically the main thing I dont like.
That's why I prefer to pass handlers like this:
container = Injector(...) .... command_bus = CommandBus(handlers=[ container.get(EnrolHandler), ]) ... command_bus.handle(Enrol(student_id=123000, course_id=666))
or even simpler:
command_bus = CommandBus(handlers=container.get(typing.List[BaseHandler]))
But I'm not sure this specific implemenation of DI supports it (I use
I think you mean Service Locator but I don't agree it is an instance of it. I think it might look like it because this gist is taken out of the context. Basically, such a
A complete example could look like this (pseudocode, let's say it's Flask):
@app.route('/api/courses/<course_id>', methods=['POST']) def enrol_view(bus: CommandBus): # bus is injected using framework integration e.g. flask-injector lib command = ... # build command out of request.json, irrelevant for the example try: bus.handle(command) except AlreadyEnrolled: return '', 409 else: return '', 204
CommandBus is used at the boundaries of the project - e.g. API views or background (Celery/Rq/whatever) tasks. It's never directly created.
The second thing that may not be obvious is that I use scopes and creating objects on-demand instead of singletons. So let's say
class EnrolHandler: _session: Session # sqlalchemy.orm.Session, a stateful object (!) def __call__(self, command: Enrol) -> None: self._session.add(...)
My bad, I meant Service Locator of course. Which container do you pass to constructor of
Besides that your examples are quite close / or almost the same what I'm doing in my code.
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
What would be the scenario when