views:

45

answers:

1

As far as I have understood, dependency injection separates the application wiring logic from the business logic. Additionally, I try to adhere to the law of Demeter by only injecting direct collaborators.

If I understand this article correctly, proper dependency injection means that collaborators should be fully initialized when they are injected, unless lazy instantiation is required. This would mean (and is actually mentioned in the article) that objects like database connections and file streams should be up and ready at injection time.

However, opening files and connections could result in an exception, which should be handled at some point. What is the best way to go about this?

I could handle the exception at 'wire time', like in the following snippet:

class Injector: 
    def inject_MainHelper(self, args): 
        return MainHelper(self.inject_Original(args)) 

    def inject_Original(self, args): 
        return open(args[1], 'rb') 

class MainHelper: 
    def __init__(self, original): 
        self.original = original 

    def run(self): 
        # Do stuff with the stream 

if __name__ == '__main__': 
    injector = Injector() 
    try: 
        helper = injector.inject_MainHelper(sys.argv) 
    except Exception: 
        print "FAILED!" 
    else: 
        helper.run() 

This solution, however, starts to mix business logic with wiring logic.

Another solution is using a provider:

class FileProvider:
    def __init__(self, filename, load_func, mode):
        self._load = load_func
        self._filename = filename
        self._mode = mode

    def get(self):
        return self._load(self._filename, self._mode)

class Injector:
    def inject_MainHelper(self, args):
        return MainHelper(self.inject_Original(args))

    def inject_Original(self, args):
        return FileProvider(args[1], open, 'rb')

class MainHelper:
    def __init__(self, provider):
        self._provider = provider

    def run(self):
        try:
            original = self._provider.get()
        except Exception:
            print "FAILED!"
        finally:
            # Do stuff with the stream

if __name__ == '__main__':
    injector = Injector()
    helper = injector.inject_MainHelper(sys.argv)
    helper.run()

The drawback here is the added complexity of a provider and a violation of the law of Demeter.

What is the best way to deal with exceptions like this when using a dependency-injection framework as discussed in the article?


SOLUTION, based on the discussion with djna

First, as djna correctly points out, there is no actual mixing of business and wiring logic in my first solution. The wiring is happening in its own, separate class, isolated from other logic.

Secondly, there is the case of scopes. Instead of one, there are two smaller scopes:

  • The scope where the file is not verified yet. Here, the injection engine cannot assume anything about the file's state yet and cannot build objects that depend on it.
  • The scope where the file is successfully opened and verified. Here, the injection engine can create objects based on the extracted contents of the file, without the worry of blowing up on file errors.

After entering the first scope and obtaining enough information on opening and validating a file, the business logic tries to actually validate and open the file (harvesting the fruit, as djna puts it). Here, exceptions can be handled accordingly. When it is certain the file is loaded and parsed correctly, the application can enter the second scope.

Thirdly, not really related to the core problem, but still an issue: the first solution embeds business logic in the main loop, instead of the MainHelper. This makes testing harder.

class FileProvider:
    def __init__(self, filename, load_func):
        self._load = load_func
        self._filename = filename

    def load(self, mode):
        return self._load(self._filename, mode)

class Injector:
    def inject_MainHelper(self, args):
        return MainHelper(self.inject_Original(args))

    def inject_Original(self, args):
        return FileProvider(args[1], open)

    def inject_StreamEditor(self, stream):
        return StreamEditor(stream)

class MainHelper:
    def __init__(self, provider):
        self._provider = provider

    def run(self):
        # In first scope
        try:
            original = self._provider.load('rb')
        except Exception:
            print "FAILED!"
            return
        # Entering second scope
        editor = Injector().inject_StreamEditor(original)
        editor.do_work()


if __name__ == '__main__':
    injector = Injector()
    helper = injector.inject_MainHelper(sys.argv)
    helper.run()

Note that I have cut some corners in the last snippet. Refer to the mentioned article for more information on entering scopes.

A: 

I've had discussion about this in the contect of JEE, EJB 3 and resources.

My understanding is that we need to distinguish between injection of the Reference to a resource and the actual use of a resource.

Take the example of a database connection we have some pseudo-code

 InjectedConnectionPool icp;

 public void doWork(Stuff someData) throws Exception{

       Connection c = icp.getConnection().
       c.writeToDb(someData); 
       c.close(); // return to pool


 }

As I understand it:

1). That the injected resource can't be the connection itself, rather it must be a connection pool. We grab connections for a short duration and return them. 2). That any Db connection may be invalidated at any time by a failure in the DB or network. So the connection pooling resource must be able to deal with throwing away bad connections and getting new ones. 3). A failure of injection means that the component will not be started. This could happen if, for example, the injection is actually a JNDI lookup. If there's no JNDI entry we can't find the connection pool definition, can't create the pool and so can't start the component. This is not the same as actually opening a connection to the DB ... 4). ... at the time of initialisation we don't actually need to open any connections, a failure to do so just gives us an empty pool - ie. exactly the same state as if we had been running for a while and the DB went away, the pool would/could/should throw away the stale connections.

This model seems to nicely define a set of responsibilities that Demeter might accept. Injection has respobilitiy to prepare the ground, make sure that when the code needs to do something it can. The code has the responsibility to harvest the fruit, try to use the prepared material and cope with actual resource failures and opposed to failures to find out about resources.

djna
I can see how in a connection pool would be suitable in this situation, but this leaves the situation when actually one 'connection' is needed. For example, when loading and parsing a configuration file at the start of an applications runtime.
ghostonline
In the case where you must do real work as part of the injection then the same rule can apply - if you can't complete the injection the applcation fails to start. So again there is a clear division of responsibilities. So two options: don't start, or require app code to be resilient to ongoing failures. I think that pure initialisation failures, of the kind you describe, should result in failure to start.
djna
And what would be the best way to go about this? My first solution handles such exceptions 'outside' of all other business logic, but that looks to me like mixing the wiring and business logic.
ghostonline
The injection engine (what ever that is) is responsible for refusing to start. If the engine encounters exceptions it knows to stop. It is told: inject this file, database connection or whatever. This has no business logic in it, it's just a resource access. I don't see mixing here.
djna
You are correct, I was distracted by the location of the exception handling. I have formulated a solution based on your hints. Thank you.
ghostonline