0

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?

1
  • 1
    Nothing 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. Commented Jul 7, 2022 at 18:25

1 Answer 1

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.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

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