3.6. Adapters

Adapters are the Zope 3 way of making re-usable components, or of re-using existing components. It is important that you make proper use of interfaces and the component architecture in order to make your code more re-usable and more loosely coupled.

Consider a Python library that is not aware of Zope. To use this in Zope 2, one would have to mix in a large number of classes just to get the basic Zope object support, making this a very difficult task. In Zope 3, you use adapters to provide the glue. You start by defining an interface that explains how you want other Zope components to talk to the library. Then you provide an adapter that can adapt an object from the non-Zope library to that interface. The Component Architecture provides the registry that makes it easy to look up the appropriate adapter for any given context to any given interface (providing one exists).

As it turns out, adapters are also very useful in creating loosely-coupled components. An adapter is registered from a given interface, to a given interface. Because interfaces can subclass each other, you can have a general adapter for a very general interface, and a more specific adapter for a more specific interface. Similarly, you can provide a very different adapter for one interface than another. Thus, you could make one adapter that makes any code expecting the IMyFunctionality interface able to talk to any object implementing IMyContentType, and a very different adapter that works on ISomeOtherContentType.

By factoring functionality out into separate interfaces implemented with separate adapters, you create slimmer, more re-usable components that are easier to understand and test. Adapters do not necessarily have to "extend" the functionality of an object in a conventional sense. Consider the following example:

Navigation tree needs to consider a large number of options, set in portal_properties. The code that builds the navigation tree performs one complex, but well-defined task: taking a catalog query and turning it into a tree of nodes that can be displayed in the navtree. The old navigation tree code used to mix these tasks together, which meant that the code was impossible to re-use. If you wanted a navtree-like structure (such as the table of contents in this reference manual), you had to write it all again!

For Plone 2.1.3, this was rewritten to use a more generic method that externalised the navtree preferences into a more generic "strategy" object. This let other objects re-use the basic algorithm using different strategies. However, the navtree strategy for the actual navigation tree was still hard-coded into Plone, so if you wanted slightly different strategies in a different context, say, you couldn't do that.

In Plone 2.5, the strategy objects were defined with the following interface in 'CMFPlone.browser.interfaces':

    class INavtreeStrategy(Interface):

        rootPath = Attribute("The path to the root of the navtree (None means use portal root)")

        showAllParents = Attribute("Whether or not to show all parents of the current context always")

        def nodeFilter(node):
            """Return True or False to determine whether to include the given node
            in the tree. Nodes are dicts with at least one key - 'item', the
            catalog brain of the object the node represents.
            """

        def subtreeFilter(node):
            """Return True or False to determine whether to expand the given
            (folderish) node
            """

        def decoratorFactory(node):
            """Inject any additional keys in the node that are needed and return
            the new node.
            """

The basic implementations for these are defined in CMFPlone.browser.navtree, e.g.:

    class SitemapNavtreeStrategy(NavtreeStrategyBase):
        """The navtree building strategy used by the sitemap, based on
        navtree_properties
        """
        implements(INavtreeStrategy)
        #adapts(*, ISiteMap)

        def __init__(self, context, view=None):
            ...

        def nodeFilter(self, node):
            ...

        def subtreeFilter(self, node):
            ...

        def decoratorFactory(self, node):
            ...

ISiteMap is the interface that is used on the view that builds the sitemap, in CMFPlone.browser.navigation. The adapts line is commented out - in later versions of Zope it will be the easiest way of specifying which object this adapter class adapts from (the implements line tells you what it adapts to), but for now we need to wire this up in ZCML. In CMFPlone/browser/configure.zcml we have:

  <adapter for="*
                .interfaces.ISiteMap"
            factory=".navtree.SitemapNavtreeStrategy"
            provides=".interfaces.INavtreeStrategy" />

Note the newline - the adapter is registered for * (any interface) and ISiteMap. Since it's registered for more than one interface, it's a multi-adapter. The factory attribute specifies the class that is used to create the adapter, and the provides attribute tells the component architecture that this provider should be loaded when someone is looking for an INavtreeStrategy.

Also note the class paths starting with a .. This means they are relative to the directory that configure.zcml is in, CMFPlone.browser in this case. You could also use a full path, e.g. Products.CMFPlone.browswer.interfaces.INavtreeStrategy, but this makes it more difficult to refactor the package layout and is harder to read.

The sitemap creation code in CMFPlone.browser.navigation adapts the context (i.e. the content object being viewed) and the view class itself to an INavtreeStrategy in order to find out which strategy to use:

  from zope.component import getMultiAdapter
  ...
  strategy = getMultiAdapter((context, self), INavtreeStrategy)

In fact, if you were using a single-context adapter, you could just instantiate it like so:

  from interfaces import IFoobar
  ...
  foobar = IFoobar(context)

Here you are instantiating the interface as if it were an actual class (it kind of is, but that's beside the point). The component architecture knows to find the appropriate adapter factory by figuring out what interfaces context implements and finding the most specific adapter from one of these interfaces to IFoobar.

Note that the factory (e.g. the __init__ method) for the IFoobar adapter must take exactly one argument - the context (i.e. the object being adapted from), for a single-item adapter. For multi-adapters, it must take one argument for each interface specified in the for attribute in the adapter directive.

Now, let's say you wanted to use a different strategy when the sitemap was being viewed on IMyContentType. All you would need to do is to register a more specific adapter:

  <adapter for=".interfaces.IMyContentType
                .interfaces.ISiteMap"
            factory=".mycontent.MyContentTypeNavtreeStrategy"
            provides=".interfaces.INavtreeStrategy" />

The various Zope 3 resources cover adapters in much greater detail, but it is important to be familiar with adapters and understand the pattern they are trying to solve. Using adapters means talking to interfaces. If you talk to interfaces and not to classes directly, you not only ensure that your interface adequately covers the uses of your code, you also make it possible to substitute different implementations in different scenarios that you never dreamt of. Similarly, by using adapters to glue components together, you can avoid making "fat" interfaces that make components harder to re-use and understand.