Pattern: optional injection

When building reusable components sometimes we may want to make dependences optional.

Let’s imagine that we are building a class which handles user authentication that we will re-use between different projects:

class AuthenticationService:
    def __init__(
        self,
        web_session: IWebSession,
        security_ctx: ISecurityCtx,
        event_bus: IEventBus,
    ) -> None:

        self.web_session = web_session
        self.security_ctx = security_ctx
        self.event_bus = event_bus

    def logout(self) -> None:
        """
        Logs out current user.
        """
        user_id = self.security_ctx.user.id
        self.web_session.flush()
        self.event_bus.emit('logged_out', { 'user_id': user_id })

    # ... more code

In some of them we are using Kafka or other event stream broker (Kinesis, Redis, maybe even RabbitMQ) to publish different events from the system for analytics (IEventBus).

But in some of our projects we don’t need such functionality. Does this mean that we must create two separate AuthenticationService classes? No.

There are two ways to handle this situation.

Optional Injection

First one is to use optional injection. We simply make the event_bus optional and check if it’s not None before calling it:

class AuthenticationService:
    def __init__(
        self,
        web_session: IWebSession,
        security_ctx: ISecurityCtx,
        event_bus: Optional[IEventBus] = None,
    ) -> None:

        self.web_session = web_session
        self.security_ctx = security_ctx
        self.event_bus = event_bus

    def logout(self) -> None:
        """
        Logs out current user.
        """
        user_id = self.security_ctx.user.id
        self.web_session.flush()
        if self.event_bus:
            self.event_bus.emit('logged_out', { 'user_id': user_id })

    # ... more code

If injectpy won’t be able to find a binding for IEventBus it will use None value instead of rising an error.

Null Object Pattern

Another way to handle this situation is to use “Null Object Pattern” - basically an implementation that does nothing:

class NullEventBus(IEventBus):
    def emit(self, event_name: str, params: Dict[str, Any]) -> None:
        # null implementation, do nothing
        pass

Now, depending on the project you may bind IEventBus either to real implementation like RedisEventBus or null one which does nothing:

kernel = Kernel()
# we use redis implementation in projects which need to publish events
kernel.bind(IEventBus, to=RedisEventBus)
# we use null implementation if we don't want to publish anything
kernel.bind(IEventBus, to=NullEventBus)

Note

Both patterns will be equally good choices in most situations. You should probably go with “Null Object Pattern” when possible though, as it doesn’t add any additional logic to the client class.