Sample Pythonic Inversion-of-Control Pseudo-Container.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 | ######################################################################
##
## Feature Broker
##
######################################################################
class FeatureBroker:
def __init__(self, allowReplace=False):
self.providers = {}
self.allowReplace = allowReplace
def Provide(self, feature, provider, *args, **kwargs):
if not self.allowReplace:
assert not self.providers.has_key(feature), "Duplicate feature: %r" % feature
if callable(provider):
def call(): return provider(*args, **kwargs)
else:
def call(): return provider
self.providers[feature] = call
def __getitem__(self, feature):
try:
provider = self.providers[feature]
except KeyError:
raise KeyError, "Unknown feature named %r" % feature
return provider()
features = FeatureBroker()
######################################################################
##
## Representation of Required Features and Feature Assertions
##
######################################################################
#
# Some basic assertions to test the suitability of injected features
#
def NoAssertion(obj): return True
def IsInstanceOf(*classes):
def test(obj): return isinstance(obj, classes)
return test
def HasAttributes(*attributes):
def test(obj):
for each in attributes:
if not hasattr(obj, each): return False
return True
return test
def HasMethods(*methods):
def test(obj):
for each in methods:
try:
attr = getattr(obj, each)
except AttributeError:
return False
if not callable(attr): return False
return True
return test
#
# An attribute descriptor to "declare" required features
#
class RequiredFeature(object):
def __init__(self, feature, assertion=NoAssertion):
self.feature = feature
self.assertion = assertion
def __get__(self, obj, T):
return self.result # <-- will request the feature upon first call
def __getattr__(self, name):
assert name == 'result', "Unexpected attribute request other then 'result'"
self.result = self.Request()
return self.result
def Request(self):
obj = features[self.feature]
assert self.assertion(obj), \
"The value %r of %r does not match the specified criteria" \
% (obj, self.feature)
return obj
class Component(object):
"Symbolic base class for components"
######################################################################
##
## DEMO
##
######################################################################
# ---------------------------------------------------------------------------------
# Some python module defines a Bar component and states the dependencies
# We will assume that
# - Console denotes an object with a method WriteLine(string)
# - AppTitle denotes a string that represents the current application name
# - CurrentUser denotes a string that represents the current user name
#
class Bar(Component):
con = RequiredFeature('Console', HasMethods('WriteLine'))
title = RequiredFeature('AppTitle', IsInstanceOf(str))
user = RequiredFeature('CurrentUser', IsInstanceOf(str))
def __init__(self):
self.X = 0
def PrintYourself(self):
self.con.WriteLine('-- Bar instance --')
self.con.WriteLine('Title: %s' % self.title)
self.con.WriteLine('User: %s' % self.user)
self.con.WriteLine('X: %d' % self.X)
# ---------------------------------------------------------------------------------
# Some other python module defines a basic Console component
#
class SimpleConsole(Component):
def WriteLine(self, s):
print s
# ---------------------------------------------------------------------------------
# Yet another python module defines a better Console component
#
class BetterConsole(Component):
def __init__(self, prefix=''):
self.prefix = prefix
def WriteLine(self, s):
lines = s.split('\n')
for line in lines:
if line:
print self.prefix, line
else:
print
# ---------------------------------------------------------------------------------
# Some third python module knows how to discover the current user's name
#
def GetCurrentUser():
return os.getenv('USERNAME') or 'Some User' # USERNAME is platform-specific
# ---------------------------------------------------------------------------------
# Finally, the main python script specifies the application name,
# decides which components/values to use for what feature,
# and creates an instance of Bar to work with
#
if __name__ == '__main__':
print '\n*** IoC Demo ***'
features.Provide('AppTitle', 'Inversion of Control ...\n\n... The Python Way')
features.Provide('CurrentUser', GetCurrentUser)
features.Provide('Console', BetterConsole, prefix='-->') # <-- transient lifestyle
##features.Provide('Console', BetterConsole(prefix='-->')) # <-- singleton lifestyle
bar = Bar()
bar.PrintYourself()
#
# Evidently, none of the used components needed to know about each other
# => Loose coupling goal achieved
# ---------------------------------------------------------------------------------
|
Inversion of Control (IoC) Containers and the Dependency Injection pattern have drawn some attention in the Java world, and they are increasingly spreading over to .NET, too. (Perhaps we are facing a sort of "Infection OUT of Control" - IooC? ;)
IoC is all about loose coupling between components of an application, about cutting off explicit, direct dependencies, plus some goodies (most of which are useful in statically typed languages only, like automatic type/interface matching). A thorough discussion on the subject can be found at http://www.martinfowler.com/articles/injection.html .
In statically typed languages, an IoC container is quite a challenge. But at the heart of it, there are only few key concepts behind it.
- Components do not know each other directly
- Components specify external dependencies using some sort of a key.
- Dependencies are resolved late, preferably just before they are used (JIT dependency resolution).
- Dependencies are resolved once for each component.
You guessed it - it should not be such a big deal to do this in python!
And indeed, a combination of a broker, descriptors and lazy attributes brings about pretty much the same core result as those IoC containers - effectively in little more then 50 lines (not counting demo code, comments and empy lines).
So what does the code do?
- It offers a mechanism to register provided "features".
- It offers a mechanism to "declare" required features in a readable way as attributes.
- The required features are resolved (injected) as late as possible - at access time.
- It provides for reasonable verification of injected dependencies.
The supported injection type is "Setter Injection", which basically means that dependencies are expressed through attributes. There is another type of injection, the "Constructor Injection", but that one builds heavily on static typing and can therefore not be employed in python (or at least I could not think of any elegant way to do it).
Of course, there are a million ways to enhance the code in this recipe. But on the other hand, isn't that true for almost any piece of code?
Cheers and happy injecting!
[See Also]
Recipe "Loose Coupling" by Jimmy Retzlaff for a basic broker implementation at http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/81983
Recipe "lazy attributes" by S�bastien Keim at http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/131495
Maybe I misunderstood the article by Martin Flower. I think what you implemented is ServiceLocator pattern. Your implementation is really cool, especially the validator of a feature.
However, to me, the FeatureBroker is a kind of ServiceLocator. So it isn't really the Inversion of control that I expected.
Kind of Point of View. The essence of ServiceLocator is that components make explicit calls to the locator to request a particular feature, so every component "sees" the locator.
DependencyInjection is characterized by dependencies being somehow "declared" by components, while the resolution mechanism is completely hidden.
In this recipe, components only need to declare what they need. They do not need to see the FeatureBroker, since that is an "implementation detail" of the dependency declaration/resolution mechanism. (The implementation can be changed anytime to search classes for RequiredFeature slots and actually inject values, as you probably expected, without affecting existing components.) From this design-level point of view, the recipe conforms to DependencyInjection.
On the other hand, the implementation itself uses the FeatureBroker as a sort of ServiceLocator, so from an implementation-level point of view, the recipe is a ServiceLocator.
In the end, it all depends on whose lawyer you are. If you are an implementation lawyer, you will think of the recipe as being a ServiceLocator. If you are a design lawyer, you will think of it as DependencyInjection. And if you are management lawyer, you will think of it in terms like "Does it get the job done, and how much does it cost?"
Also, statically typed languages do not support concepts like attribute descriptors, decorators etc. The strict separation of the two patterns is partly, if not mainly, enforced by the static nature of those languages.
In Python, thank to its dynamic features, it is possible to "adjust" how an attribute is retrieved and encapsulate that "adjustment" away from clients. It then becommes apparent that the two patterns are merely two manifestations of the same abstraction.
There is a bug in the method HasMethods. The line "return callable(attr)" should be replaced with "if not callable(attr): return False"
A silly error indeed - thanks. I have applied your fix to the recipe
I have written an alternative approach which does not depend on an global FeatureBroker. It's extremly simple, thanks to the power of the snake ;D
http://www.dennis-kempin.de/python/dependency-injection-in-python/
Thanks for inspiration.
I have provided implementation of Non-invasive Constructor Injection in Recipe 576609.
Hello, thank you for your template! Please, why does this little modification not work:
Now i get the Asserting Error: assert name == 'result', "Unexpected attribute request other then 'result'
THANKS!
I see a few occurrences of a spelling mistake that is quite unpleasant to me, because it is so widespread: the use of "then" when "than" should be used.
"other then 'result'" should be "other than 'result'"
"little more then 50 lines" should be "little more than 50 lines"
please look at http://theoatmeal.com/comics/misspelling
Great snippet. Congrats. The only thing unpleasant(and can be easily fixed) is the missing
os
module import. Please add it.Service Locator is an antipattern, as you still don't know what your class "Bar" requires. You aren't injecting your dependencies. You are looking for them. The dependency injection pattern is supposed to make testing easier and more efficient. You pass in your dependencies (as mocks). By passing in dependencies, you don't need to know implementation details, which makes your tests more robust.
Perhaps I am missing something but what I miss in python for dependency injection is that you would need to scan all libraries available in python path and source them. In Java that would be done by spring or other, but in python by default no library is loaded untill it is explicity sourced and as consumer I do not know the provider and do not want to know it (that is why I am using IoC) I am not going to import or source it to make this work.
This is de piece I miss for this being really useful