Personal tools
You are here: Home Documentation Tutorials A Grok-Centric Explanation of Adaptation

A Grok-Centric Explanation of Adaptation

Note: Return to tutorial view.

Adaptation is a central concept in the Zope 3 Component Architecture. This explanation approaches it from a Grok perspective.

Adapters from an MVC architecture perspective

How do Adapters compare to the MVC architecture?

In Grok you have View and Model components. Applications written in them are also likely to generate additional code that needs to sit between your Views and your Models. Adapters are one solution for keeping this code out of your Models or Views and putting them into a place where they can easily be reused. Put as simply as possible, an Adapter takes the Interface of an existing component and adapts it to provide another Interface.

If you are used to other web frameworks that use a Model-View-Controller (MVC) architecture, such as Ruby on Rails, you may be thinking, "Isn't the glue code between the Model and the View called a Controller?" Adapters differ from common MVC web architecture in that your Views can either access the Model directly, or extend the Model using one or more Adapters as intermediate actors. Adapters differ from Controllers in that they can adapt any type of component, and not just a Model component.

The Zope Component architecture approach page says, "The main idea in the Zope Component Architecture is the use of components, rather than multiple-inheritance for managing complexity". One of the benefits of Adapters over multiple-inheritance is that you can extend the behavior of a Class without needing to modify the code for that Class. If you are acting upon a component provided by someone else, and that component has a well defined interface, then adaption is a much more robust way of extending the behaviour of that component than using meta programming techniques such as monkey patching.

A real world example of Adaption

Imagine that you are about to give a presentation at a meeting. You've brought your laptop, and the room has a projector and a cable to plug your laptop into the projector. However, the cable you brought won't plug into the projector because your cable only interfaces with a DVI port, and the projector only interfaces with a VGA port.

So you ask of the room, "does anyone have a DVI to VGA adapter?"

An adapter? Let's state the problem of the projector and the laptop in Grok. Of course it's impossible to implement a hardware problem in software (duh!) so for this example we won't have any implementation details:

import grok
from zope.interface import Interface

class IVGAPort(Interface):
    "Video connection using VGA"

    def connectToVGA(cable):
        "Connect to a VGA video cable"

class IDVIPort(Interface):
    "Video connection using DVI"

    def connectToDVI(cable):
        "Connect to a DVI video cable"

class DVIToVGAAdapter(grok.Adapter):
    "Use your DVI laptop with a VGA projector"
    grok.adapts(IDVIPort)
    grok.provides(IVGAPort)

    def connectToVGA(cable):
        # self.context will be a reference to the laptop object

def plugLaptopIntoProjector(laptop, projector):
    "Display your presentation on the wall"

    vga_capable = IVGAPort(laptop, None)
    if vga_capable is None:
        print "Uh oh, looks like you forget to bring an adapter."
    else:
        vga_capable.connectToVGA(projector)

A common idiom for asking the Zope 3 component architecture to perform adaptation is to instantiate a new Interface with the object that you wish to adapt as an argument. We did this in the 'plugLaptopIntoProjecter' function with the line:

vga_capable = IVGAPort(laptop, None)

The vga_capable variable is now an object that provides the IVGAPort. If the laptop already implements IVGAPort, then this will be the laptop object itself. If the laptop provides IDVIPort, then the DVIToVGAAdapter will be used to extend IDVIPort to provide IVGAPort. The second optional argument (None) will be returned if the laptop doesn't provide an IVGAPort and no suitable adapter can be found. If adaptation was successful, you shouldn't care if you have a Laptop object or a DVIToVGAAdapter object returned - your only concern is that you have an object that provides the requested IVGAPort interface that provides the necessary connectToVGA(projector) method.

The DVI to VGA Adapter

Let's review the DVIToVGAAdapter class that we created.

class DVIToVGAAdapter(grok.Adapter):
    "Use your DVI laptop with a VGA projector"
    grok.adapts(IDVIPort)
    grok.provides(IVGAPort)

    def connectToVGA(cable):
        # self.context will be a reference to the laptop object

When you inhert from grok.Adapter, Grok will do two things. One is to register your Adapter with the Zope 3 Component Architecture so that it can look-up the right adapter for the requested interface. The second is to reference the object that is being adapted to an attribute named context. In Grok, the Adapter base class that you inherit from is very simple and looks like this:

class Adapter(object):

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

This is just a convention used to save some typing. If you wish, you may supply your own constructor to call the object being adapted something specific to your adapter. For example, in the above example, we could add the following constructor to our DVIToVGAAdapter Class:

def __init__(self, laptop):
    self.laptop = laptop

Adaptation is about requiring an object which provides one interface, and extending it so that you get a new interface. We declare that we require an object that provides an IDVIPort interface - this is declared with the directive grok.adapts(IDVIPort). We are extending the IDVIPort interface to provide a new interface, IVGAPort - this is declared with the directive grok.provides(IVGAPort). Finally, the adapter is responsible for fulfilling the interface that it declares it implements. This implementation will typically use self.context attribute - which is the object that is being adapted.

It is possible to use either grok.provides(IVGAPort) or grok.implements(IVGAPort) directives for the DVIToVGAAdapter. In a more complex example it's possible that an adapter provides multiple interfaces, but only one interface is meant to be used for the purposes of adaptation. The grok.provides directive explicitly declares the interface to be used for adaptation, however if an Adapter only declares that it implements a single interface using grok.implements then this interface will implicitly be used for adapation.

Adapters in a Web Application

While using physical adapters to connect laptops is something that we can understand, where would using adapters in a web application make sense?

While adapters can perform any action, one use for them is to add additional, reusable HTML specific functionality to your application.

Let's say that you have a Model component called Article. You've developed two different Views for your Article: PlainArticleView and FancyArticleView. Your Article has a keywords property, and you'd like to render the HTML meta tag using the keywords from an Article. You might write:

class PlainArticleView(grok.View):
    # lots of other code here
    def meta_tags(self):
        if len(self.context.keywords) > 0:
            return '<meta name="keywords" content="%s" />' % ','.join(
                self.context.keywords()
            )

class FancyArticleView(grok.View):
    # lots of other code here
    def meta_tags(self):
        if len(self.context.keywords) > 0:
            return '<meta name="keywords" content="%s" />' % ','.join(
                self.context.keywords()
            )

Well, that doesn't look so good does it? We've stated the logic of how to convert our keywords into a meta tag in two different places. Oh the horrors, what will all those DRY loving Web 2.0 programmers think of us? Let's write an Adapter that can render our meta tags:

class IMetaTags(Interface):
    def render(self):
        "HTML Meta Tags"

class MetaTags(grok.Adapter):
    "HTML Meta tags"
    grok.implements(IMetaTags)
    grok.adapts(IArticle)

    def render(self):
        if len(self.context.keywords) > 0:
            return '<meta name="keywords" content="%s" />' % (
                ','.join(self.context.keywords()
            )

class PlainArticleView(grok.View):
    # lots of other code here
    def meta_tags(self):
        return IMetaTags(self.context).render()

class FancyArticleView(grok.View):
    # lots of other code here
    def meta_tags(self):
        return IMetaTags(self.context).render()

Now our presentation logic is stated in only one place, which is good. Although you might be thinking, "Is this worth all the trouble, my PHP-loving Web 1.0 programmers are saying I'm just making my code harder to understand." This is a good point, and sometimes KISS is the enemy of DRY. But let's say that our application is expected to have a long lifecycle, where you know you'll be wanting to extend the application functionality frequently. Let's see how this extra work we have done can help us down the road. You've added a description property to your Article, and you'd like to have a description meta tag. So you change the MetaTags adapter to look like this:

class MetaTags(grok.Adapter)
    "XHTML Meta tags"
    grok.implements(IMetaTags)
    grok.adapts(IArticle)

    def render(self):
        html = ''
        if len(self.context.keywords) > 0:
            html += '<meta name="keywords" content="%s" />' % (
                ','.join(self.context.keywords()
            )
        if len(self.context.description) > 0:
             html += '<meta name="description" content="%s" />' % (
                 ','.join(self.context.keywords()
             )
        return html

Now both of your Views have been updated, and you only need to change the code in one place. Later on you add new HowTo Model to your application, and you create some Views for it. You are careful to make sure that your HowTo model has both a keywords and a description property, and now you just need to allow your adapter to work with both the HowTo and the Article models. Interfaces can inherit from other interfaces, just like a normal Python Class, so you can make explicit the relationship between your metadata and your content types.

from zope.interface import Interface
from zope.schema import List, TextLine, SourceText

class IMetadata(Interface):
    "Metadata about a content type"
    description = TextLine(title=u'short summary of the content',)
    keywords = List(
                 title=u"Keywords",
                 unique=True,
                 value_type=TextLine(title=u"Keyword"),
                 required=False
               )

class IArticle(IMetadata):
    body = SourceText(title=u'Article Body')

class IHowTo(IMetadata):
    body = SourceText(title=u'HowTo Body')

Finally you just need to change the line of your MetaTags adapter from grok.adapts(IArticle) to grok.adapts(IMetadata). Now all of the Views that you write for your HowTo can reuse the code that you were using to support the Views for your Article.

Further Reading

There are some more documents that can help you grasping the concept of adaptation

This tutorial is taken from a Kevin Teague blog entry

zope.component/README.txt A discussion on Adapters that shows more API examples of adapters in action.

Adaptation For Busy People A description of the purpose and usage of Zope's adaptation machinery in fewer than 1500 words.

Using Grok to walk like a duck A presentation that takes you through different design paradigms to enhance code reusability. Good to compare adaptation with other reuse techniques common in Python.