0

I'm finding the use of IoC containers to be quite the shift in my application design, and its for the better. I'm using a framework called injector that aims to mimic (albeit not entirely) the Guice framework.

With that in mind, I have 4 classes that I'd like to outline that show where my mixing of container use and "poor mans" (manual) DI take place:

@singleton
class App:

    @inject
    def __init__(self, config: Config, logger: Logger) -> None:
        self.config = config
        self.logger = logger

@singleton
class Config:

    def __init__(self, path) -> None:
        self.path = path

@singleton
class Logger:

    def __init__(self, path) -> None:
        self.path = path

@singleton
class Client:

    def __init__(self, config: Config, logger: Logger) -> None:
        self.config = config
        self.logger = logger

In production I use a module to tell the container how to instantiate Config and Logger:

class AppModule(module):

    CONFIG_PATH = '/foo/bar/settings.config'
    LOGGER_PATH = '/foo/bar/logs'

    @provider
    def provide_config(self) -> Config:
        return Config(CONFIG_PATH)

    @provider
    def provide_logger(self) -> Logger:
        return Logger(LOGGER_PATH)

The entry point of the program looks like this:

from program import App
from program import AppModule

if __name__ == '__main__':

    injector = Injector(AppModule)
    app = injector.get(App)
    app.start()

Now the start method for the App class would in practice look as:

def start(self) -> None:
    self.config.load()
    Client(self.config, self.logger).run()  # is a blocking operation for a GUI to run
    self.end()

def end(self) -> None:
    self.config.save()

It is the start method that "poor mans" DI takes place. I never define to the IoC container how a Client object is instantiated (hence no @inject) and it uses the same singleton Config and Logger that App uses. The IoC container is responsible for injecting the appropriate dependencies into the App class, but afterwards I explicitly pass them to a new instance of Client. I do this because App does not depend on Client (right?) given that none of its member functions ever need to reference it again beyond start.

If I were to define it in the IoC container (through AppModule) I would have to somehow reference the Injector instance inside App which would mean passing it around. To my knowledge, Guice discourages this and it is bad practice for DI.

Therefore, is it normal or problematic to use manual DI in tandem with an IoC container?

1 Answer 1

1

I'll try to address some points you make in your post before answering your question more directly :

I do this because App does not depend on Client (right?) given that none of its member functions ever need to reference it again beyond start.

False. App is completely dependent on Client, since it cannot work without it! If you delete Client right now, your app simply won't run, since App is missing its dependency. It doesn't look that way because you made your Client a Singleton, and pull a reference from it only in App.start, but don't be mistaken : it is still a direct dependency.

If I were to define it in the IoC container (through AppModule) I would have to somehow reference the Injector instance inside App which would mean passing it around. To my knowledge, Guice discourages this and it is bad practice for DI.

It is bad practice to reference the injector in your code, doing so would simply make it a Service Locator, which defeats the purpose of using the framework. I think you're too focused on doing the DI yourself for Client. What if we did use your DI framework to wire up Client? Then :

@singleton
class Client:

    @inject
    def __init__(self, config: Config, logger: Logger) -> None:
        self.config = config
        self.logger = logger

You (probably) don't even need to do anything else, since you already wrote your providers for Config and Logger.

Then, in App.start, simply pass in an instance of Client :

def start(self, client: Client) -> None:
    self.config.load()
    client.run()
    self.end()

Since App only needs Client for its start method.

Personnally, I'd probably go with making client a member of App, and letting your DI framework do the wiring :

@singleton
class App:

    @inject
    def __init__(self, config: Config, logger: Logger, client: Client) -> None:
        self.config = config
        self.logger = logger
        self.client = client

Then you'd just have to use self.client in App.start, and the dependency App -> Client would be clearer in my opinion, but your mileage may vary.

Therefore, is it normal or problematic to use manual DI in tandem with an IoC container?

At least in my experience, it's not frequent to want to do manual DI while also using a DI framework/library. It does lead to some problems, as you have encountered. The framework is there to do this job for you, so if you want to do that job manually too, you're bound to run into some trouble with the framework. For your case, I'd simply let Client be injected by the framework, and that should solve your problems for now.

But I'd also look into removing all those pesky @singleton too, since they cause a lot more problem than they solve and you can setup your whole application with only one copy of each of your classes through your DI framework.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.