An introduction to the Zope Component Architecture
If you are active in the field of Web Applications or Content Management Systems the chance is high that you once heard about Zope, the Python powered Web Application Server (which is also the basis for Plone). What you eventually might not have heard of yet is the Zope Component Architecture.
The Zope Component Architecture (ZCA) is a small part which was factored out of Zope3 and is available as a Python Eggs on the Cheeseshop. In particular the eggs in question are zope.interface and zope.component. If you don’t know what Python Eggs are and what you can do with it, please consult my little introduction I wrote a while back.
It’s about interfaces
The central part of the Zope Component Architecture is an interface. An interface describes the public methods and attributes of a component (which is usually a normal Python class). Let’s look at a class which provides an address:
class Person(object):
"""a person"""
def __init__(self, firstname, lastname, address, zip, city, dob):
"""initialize the address"""
self.firstname = firstname
self.lastname = lastname
self.address = address
self.zip = zip
self.city = city
self.dob = dob
def fullname(self):
return "%s %s" %(self.firstname, self.lastname)
This apparently is a very basic class but the question here is still what you should assume as it’s public interface. Are you allowed (or supposed) to use firstname or lastname etc.? Are you allowed to use fullname()? So it’s not clear what is public and what is implementation specific. Interfaces can help with that.
Let’s define an interface for it:
from zope.interface import Interface, Attribute
class IPerson(Interface):
"""a Person"""
firstname = Attribute("""the firstname of a person""")
lastname = Attribute("""the lastname of a person""")
address = Attribute("""the address of a person""")
zip = Attribute("""the zip code of a person""")
city = Attribute("""the city of a person""")
dob = Attribute("""the date of birth of a person""")
def fullname():
"""returns the full name of this person"""
As you can see an interface is also just a class but it derives from Interface. It defines all the methods and attributes of that class which are supposed to be public (in our case it’s everything).
What’s left to do is to associate the class above with that interface. To do this we put this interface into interfaces.py (this is some sort of convention, it can also be a folder called interfaces/ with all the needed interfaces in modules inside) and then we adjust the class as follows
from zope.interface import implements
from interfaces import IPerson
class Person(object):
implements(IPerson)
...
Now what does that bring us? Right here not that much in terms of functionality but maybe in terms of coding standards, because
Interfaces let you declare the public interface of class
Interfaces are a starting point when trying to understand a class structure
Starting with an interface makes you think more about the structure of the component you are trying to write
Please also note that during runtime it’s not tested if the interface is really implemented by that class. All what is done is to associate the interface name (and also the path to it like e.g. pyogp.lib.interfaces.IPerson) with that class. In unit tests though you can also test whether the methods are implemented or not which makes it easier to spot whether you have completed your work or not (and maybe have made typos or did some other mistake).
The ZCA now uses interfaces also do have another layer of abstraction available because you now can reference certain components by it’s interface and not by it’s class name. This means that you can more easily replace the implementation without the rest of the application knowing about it (without that you might need to use subclassing but that gets messy if you do that too much as we experiences in Zope2).
So here is what can be done with it:
Utilities
One of the easier things you can do is to register certain components as utilities by it’s interface. Let me show an example to make this clearer. Imagine you need some importer which imports person objects from a text file. What you first do now is to create an interface:
from zope.interface import Interface
class IPersonImporter(Interface):
"""imports IPerson objects"""
def import(file):
"""import person objects from a file. returns a list of IPerson"""
Now you can implement this interface in whatever way you want by simply creating a class and assigning this interface to it like we’ve seen above in the Person/IPerson example (using implements(IPersonImportert))
class PersonImporter(object):
"""an implementation if IPersonImporter"""
implements(IPersonImporter)
...
To make this a utility we have to register it as such:
from zope.component import getGlobalSiteManager gsm = getGlobalSiteManager() pi = PersonImporter() gsm.registerUtility(pi)
This first retrieves the global registry (the global site manager) and then registers the utility with it. Now how can this be used? Here is how:
from zope.component import getUtility pi = getUtility(IPersonImporter) people = pi.import()
So what is done here is that we ask for a utility which implements the IPersonImporter interface. As we registered the PersonImporter class for this before we will get this and can call import() on it. The advantage of using interfaces here is that another component can easily exchange this implementation by their own CustomPersonImportert class. This class might even return other objects than Person objects but can instead return ComplexPersonObjects which would still work as long as they also implement IPerson (and the code which uses it also just relies on the methods and attributes defined in the IPerson interface).
Adapters
Another feature of the ZCA are adapters. Adapters are mainly used to factor certain functionality out of a class. You might have noticed that classes tend to grow quickly the longer a project lasts because more and more functionality is put into them. This makes classes quite huge, hard to understand, hard to test and hard to debug. It also makes it very difficult to change a certain subset of it’s functionality. The only thing you usually can do is to subclass from this huge class and override the methods you want. The problem then is though to let the rest of the library or application use that modified class. To do this maybe you need to subclass more classes and so on. This can get messy quickly.
What we want instead is a set of well defined and small components which have a defined interface, are easy to test and to debug and are easily replaced without touching the code which uses them.
Here we can again use a layer of indirection given by interfaces. So instead of putting all the functionality in one class we put them in so called adapters. But let me give you an example:
In the IPerson interface above we do not have a method to compute the age of a person. We have the date of birth though and we can use that to do it. We of course can extend the interface and the class but imagine that the class is in use already. Depending on the situation you might have to change it in all places then. What we do instead is to create an adapter which takes the original object and computes the age. It’s sort of a wrapper around the original class. Of course we start by defining an interface for it which is rather simple:
from zope.interface import Interface
class IPersonAge(Interface):
"""computes the age of a person"""
def age():
"""return the age of a person"""
This is pretty straightforward (not that we of course also might have defined this as attribute). Next we have to write an implemenation for it:
from zope.interface import implements
from interfaces import IPersonAge
from DateTime import now
class PersonAge(object):
implements(IPersonAge)
def __init__(self, context):
self.context = context
def age(self):
return (now-self.context.dob).years() # this is some fictional API, was too lazy to look it up
So what you see here is a class which takes one argument in it’s constructor. This needs to be the object which implements IPerson (usually an object of our Person class above). We store this as “context” in our object instance. Then we of course implement the age() method and use that context (the IPerson object) to somehow compute the age.
Please also note the <code>implements</code> line where we say that this class actually implements the interface IPersonAge. Also note the adapts line this defines which objects are allowed as the context (in this case IPerson objects).
So what this means in plain words is: “I can convert objects implementing IPerson to objects implementing IPersonAge”.
So how is this used?
person = Person(....) # implements IPerson personage = IPersonAge(person) # convert if to an object IPersonAge print personage.age() # now we can use a method from IPersonAge
One important thing to make this work is missing though: We first need to register this adapter with the registry. To do this we write:
from zope.component import provideAdapter from zope.component import getGlobalSiteManager gsm = getGlobalSiteManager() gsm.registerAdapter(PersonAge, (IPerson,), IPersonAge)
Here we again use the global site manage (the global registry) to make the adapter known to the python environment. The syntax of registerAdapter is the factory (can be a class as in this example but can also be a function which returns some adapter), a tuple of interfaces on which this adapter works and the interface which the adapter provides.
This way you can deconstruct a complex class into separate components which are only losely coupled. In Plone for instance we more and more tend to only define the basic type as the class and add all behaviour via adapters (now it probably would make sense to also define the type definition in a component as this is the most likely part you want to change).
How can I override components?
Registering those adapters and utilities via Python is only one way to do it. There is also a XML syntax called ZCML which can do this. This is what’s used in Zope and Plone.
Now the question might be how you actually redefine an adapter, how you change the registry. That’s quite simple as the only thing you need to do is to register it again. The old entry will then be overwritten by your new entry and every occurrence of IPersonAge(something) will use your adapter.
For a more in depth explanation and a overview of all the functions of the Zope Component Architecture, have a look at this comprehensive manual.


