ecoe ecoe - 1 year ago 127
Python Question

Python Dependency Injection for Lazy Callables

In programming for fun, I've noticed that managing dependencies feels like a boring chore that I want to minimize. After reading this, I've come up with a super trivial dependency injector, whereby the dependency instances are looked up by a string key:

def run_job(job, args, instance_keys, injected):
args.extend([injected[key] for key in instance_keys])
return job(*args)

This cheap trick works since calls in my program are always lazily defined (where the function handle is stored separately from its arguments) in an iterator, e.g.:

jobs_to_run = [[some_func, ("arg1", "arg2"), ("obj_key",)], [other_func,(),()]]

The reason is because of a central
main loop
that must schedule all events. It has a reference to all dependencies, so the injection for
can be passed in a dict object, e.g.:

# inside main loop
injection = {"obj_key" : injected_instance}
for (callable, with_args, and_dependencies) in jobs_to_run:
run_job(callable, with_args, and_dependencies, injection)

So when an event happens (user input, etc.), the main loop may call an
on a particular object who reacts to that input, who in turn builds a list of jobs for the
main loop
to schedule when there's resources. To me it is cleaner to key-reference any dependencies for someone else to inject rather than having all objects form direct relationships.

Because I am lazily defining all callables (functions) for a game clock engine to run them on its own accord, the above naive approach worked with very little added complexity. Still, there is a code stink in having to reference objects by strings. At the same time, it was stinky to be passing dependencies around, and constructor or setter injection would be overkill, as would perhaps most large dependency injection libraries.

For the special case of injecting dependencies in callables that are lazily defined, are there more expressive design patterns in existence?

Answer Source

I've noticed that managing dependencies feels like a boring chore that I want to minimize.

First of all, you shouldn't assume that dependency injection is a means to minimize the chore of dependency management. It doesn't go away, it is just deferred to another place and time and possibly delegated to someone else.

That said, if what you are building is going to be used by others it would thus be wise to include some form of version checking into your 'injectables'so that your users will have an easy way to check if their version matches the one that is expected.

are there more expressive design patterns in existence?

Your method as I understand it is essentially a Strategy-Pattern, that is the job's code (callable) relies on calling methods on one of several concrete objects. The way you do it is perfectly reasonable - it works and is efficient.

You may want to formalize it a bit more to make it easier to read and maintain, e.g.

from collections import namedtuple

Job = namedtuple('Job', ['callable', 'args', 'strategies'])

def run_job(job, using=None):
    strategies = { k: using[k] for k in job.strategies] }
    return job.callable(*args, **strategies)

jobs_to_run = [
  Job(callable=some_func, args=(1,2), strategies=('A', 'B')),
  Job(callable=other_func, ...),

strategies = {"A": injected_strategy, ...}
for job in jobs_to_run: 
   run_job(job, using=strategies)

# actual job
def some_func(arg1, arg2, A=None, B=None):

As you can see the code still does the same thing, but it is instantly more readable, and it concentrates knowledge about the structure of the Job() objects in run_job. Also the call to a job function like some_func will fail if the wrong number of arguments are passed, and the job functions are easier to code and debug due to their explicitely listed and named arguments.

Recommended from our users: Dynamic Network Monitoring from WhatsUp Gold from IPSwitch. Free Download