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?