[Grok-dev] Alternatives to Grok for Interface based dependency injection

Ethan Jucovy ejucovy at gmail.com
Fri May 22 11:07:39 EDT 2009


On Fri, May 22, 2009 at 10:00 AM, Chris McDonough <chrism at plope.com> wrote:
> These days, I write the code using a *default* implementation as a fallback and
> I register *no* ZCML:
>
>     import feedparser
>     from myproject.interfaces import IFeedParser
>     from zope.component import queryUtility
>
>     def get_feed_entries(feed_url):
>         parser = queryUtility(IFeedParser, default=feedparser.parse)
>         parsed = parser(feed_url)
>         return parsed.entries
>
> The salient difference above is that because I use queryUtility with a default
> that is the "real" implementation instead of using getUtility, I don't need to
> do any ZCML registrations; the tests work the same.
>
> I don't know if this is meaningful for your case in particular, but I think it's
> a pattern worth describing anyway.

Hope this isn't veering too off-topic for this list, but I've been
thinking along a related line lately, though not for testing
specifically.  In particular, I was looking at zope.i18n a few weeks
ago, so I'll use it as a concrete example.

>From http://svn.zope.org/zope.i18n/trunk/src/zope/i18n/negotiator.py
{{{
class Negotiator(object):
    implements(INegotiator)

    def getLanguage(self, langs, env):
        envadapter = IUserPreferredLanguages(env)
        userlangs = envadapter.getPreferredLanguages()

        # Prioritize on the user preferred languages.  Return the
        # first user preferred language that the object has available.
        langs = normalize_langs(langs)
        for lang in userlangs:
            [etc]
}}}

I had a set of available languages (`langs` in the getLanguage
signature) and a set of user-preferred languages which I calculated
"outside the ZCA" with custom code that I hadn't registered as
IUserPreferredLanguages, since it was only applicable under certain
conditions that would've been pointlessly convoluted to express as a
request adapter.

That meant I couldn't actually use INegotiator, either through the CA
or by instantiating zope.i18n.negotiator.Negotiator directly, even
though INegotiator.getLanguage "sounds like" exactly what I needed.

I think the problem here is similar to what Alec is describing: the
function signature masks a "hidden dependency" on a CA registration of
an adapter from IRequest to IUserPreferredLanguages.  In a way that's
fine, since you're not really going to know to write
`getComponent(INegotiator).getLanguage(langs, request)` in the first
place unless you already know about these CA relationships.

But then the only alternative is to actually copy/fork the code of
`getLanguages` and omit the first two lines!  So in a way I think the
method is trying to do too many things at once -- a single function is
doing a very framework-specific switch on a request object to
determine a value, and then calculating an output with very
framework-neutral logic.

So what I've been increasingly trying to do with my code is to keep
all the switches consolidated into one function, which then calls a
second function that's "purely" logic.  That way, when I'm consuming
the code, I can choose whether or not to go through the framework
(which is also helpful for testing).  In the zope.i18n case that could
be:

{{{
class Negotiator(object):
    implements(INegotiator)

    def getLanguage(self, langs, env):
        envadapter = IUserPreferredLanguages(env)
        userlangs = envadapter.getPreferredLanguages()
        return self.calculateLanguage(langs, userlangs)

    def calculateLanguage(self, langs, userlangs):
        [etc]
}}}

Though I think the "ideal" version of this design would be even more
decoupled, since the switching function here would have to be
reproduced for every implementation of INegotiator.  I don't know what
that would look like at all; maybe various adapters registered to
INegotiator, or maybe a Negotiator base class ..

>> On 5/22/09 9:14 AM, Alec Munro wrote:
>> I'm sure best practices could be established that would
>> avoid this ever becoming problematic, but if possible, I would rather
>> use a dependency injection framework that will insist I keep my
>> dependencies properly specified.

I don't share this feeling myself.  At any rate, if the framework
required me to write my code in the way I'm describing, I'd never get
any code written. :)  I think the fact that grok/zope doesn't insist
on any sort of code organization like this is a big strength, and
comes from the same mentality as Python's own "no such thing as truly
private attributes" rule.  Chris's `get_feed_entries` code, for
example, seems completely reasonable to me, even though I think it's
technically doing the same mixing of switches and logic; if I tried to
rewrite it according to the pattern I'm describing I'd just be adding
noise.

Ethan


More information about the Grok-dev mailing list