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.