I am learning about Dependency Injection and I have been recently implementing the following classes for an app that executes commands over ssh using Python. I am confused about whether I am using it in the wrong way.
I am using a ConnectionFactory
class to create an ssh
object that I call dynamic, because it depends on the user input (username, password).
Below is a working simplified example that uses injector
and paramiko
packages.
My question is whether it's a code smell, the fact that I am instantiating manually RemoteBashCommandRunner
along with its dependencies.
from injector import Injector, inject
from paramiko import SSHClient, AutoAddPolicy
class A: pass
class B: pass
class C: pass
class D: pass
HOST, USERNAME, PASSWORD = "localhost", "username", "password"
class ConnectionFactory:
@inject
def __init__(self, a: A, b: B):
self.a, self.b = a, b
def get_connection(self, host):
# in a real implementation, this method calls the dependencies set in constructor
ssh = SSHClient()
ssh.set_missing_host_key_policy(AutoAddPolicy())
ssh.connect(host, 22, USERNAME, PASSWORD)
return ssh
class RemoteBashCommandRunner:
def __init__(self, ssh: SSHClient, c: C, d: D) -> None:
self.ssh = ssh
self.c, self.d = c, d
def run(self, command: str):
_, stdout, _ = self.ssh.exec_command(command)
print(stdout.read())
def main():
container = Injector()
connection_factory = container.get(ConnectionFactory)
ssh = connection_factory.get_connection(HOST)
runner = RemoteBashCommandRunner(ssh, C(), D())
runner.run("echo hello")
main()
I have been also thinking about binding the ssh
object in the container and getting an instance of RemoteBashCommandRunner
by using the container
.
...
class RemoteBashCommandRunner:
@inject
def __init__(self, ssh: SSHClient, c: C, d: D) -> None:
...
container.binder.bind(SSHClient, to=lambda: ssh)
container.get(RemoteBashCommandRunner).run("echo hello")
Or maybe just remove the ssh
object from the constructor, and use it as an argument in the method.
class RemoteBashCommandRunner:
@inject
def __init__(self, c: C, d: D) -> None:
def run(self, ssh, command: str):
# no more manual binding
container.get(RemoteBashCommandRunner).run(ssh, "echo hello")
Which one of the 3 approaches is better?
-
1Nothing wrong with manually creating and injecting dependencies - this is literally what dependency injection is. DI containers are just optional helper objects that do this for you and help manage instance lifetime, they are not required, and are extraneous to the dependency injection idea. As for constructor injection vs method injection - typically go for constructor injection (it indicates that the thing you're passing in is some fundamental dependency of your object); use method injection if you want to use the same object, but pass different things to it at different call sites.Filip Milovanović– Filip Milovanović2022年07月07日 18:25:50 +00:00Commented Jul 7, 2022 at 18:25
1 Answer 1
Nothing here is wrong. Manual, or Pure DI can be mixed with DI containers. The only problem is if the mixing causes confusion.
Manual DI provides more freedom than containers do. So beware having enough rope to hang yourself. One of the fundamental rules here is to do construction as high up the call stack as you can. Your example does it in main()
so no worries there.
One of the advantages of DI containers is they tend to make you separate construction from behavior. Which is a good thing. But something you can do without the container if you have a little discipline. Some prefer to have the discipline forced on them.
While you technically aren't separating construction from behavior, you are doing it in the one place I permit it: main()
.
The classic manual DI pattern is to have a bunch of lines that construct all the static objects in main()
and then follow them with one line of behavior code that starts the whole object graph ticking. You're following that pattern just fine.
Dynamic objects, ones that are born later or don't live as long as main()
does, must be constructed further down the call stack. That's fine. Just build them as high in the stack as you can and try to separate their construction from behavioral code.
Follow those rules and your manual DI shouldn't cause too many problems. Get good enough at it and you'll start to think of DI containers as overdesigned factories.
Explore related questions
See similar questions with these tags.