Newcomer
When the Component Architecture for Zope was first thought about, it was intended as an extension to Zope 2, not a replacement as it developed to become. The issue was that the existing Zope 2 API was too bloated and inconsistent, due to constant feature additions, bug fixes and coding inconsistencies. The extremely bad practice of “monkey patching” became a normality among developers to overcome the limitations of the API and fix bugs. Monkey patching is a method of overwriting library functions and class methods after importing them, which is a powerful, but dangerous, side effect of loosely-typed scripting languages.
Another motivation was to incorporate the lessons learned from consulting jobs and building large Web applications, which demonstrated the practical limits of simple object inheritance. The need for a more loosely-connected architecture arose with many objects having rather tiny interfaces in contrast to Zope 2’s few, large objects. This type of framework would also drastically reduce the learning curve, since a developer would need to learn fewer APIs to accomplish a given task.
All these requirements pointed to a component-oriented framework that is now known as the “Component Architecture” of Zope 3. Many large software projects have already turned to component-based systems. Some of the better-known projects include:
However, while Zope 3 has many similarities with the above architectures, thanks to Python certain flexibilities are possible that compiled languages do not allow.
In this chapter I will give you a high-level introduction to all component types. Throughout the book there will be concrete examples on developing most of the component types introduced here.
Services provide fundamental functionality without which the application server would fail to function. They correspond to “Tools” in CMF (Zope 2) from which some of the semantics were also inspired.
Services do not depend on other components at all. You only interact with other components by passing them as arguments to the constructor or the methods of the service. Any given application should have only a few services that cover the most fundamental functionality. When dealing with locality, services should always delegate requests upward - up to the global version of the service - if a request can not be answered locally.
The most fundamental services are the registries of the components themselves. Whenever you register a class as a utility using ZCML, for example, then the class is registered in the “Utility Service” and can be retrieved later using the service. And yes, we also have a “Service Service” that manages all registered services.
Another service is the Error Reporting service, which records all errors that occurred during the publication process of a page. It allows a developer to review the details of the error and the state of the system/request at the time the error occurred.
A convention for service interfaces is that they only contain accessor methods. Mutator methods are usually implementation-specific and are provided by additional interfaces. A consequence of this pattern is that services are usually not modified once the system is running. Please note though that we strongly discourage developers from writing services for applications. Please use utilities instead.
Services make use of acquisition by delegating a query if it cannot be answered locally. For example, if I want to find a utility named “hello 1” providing the interface IHello and it cannot be found at my local site, then the Utility Service will delegate the request to the parent site. This goes all the way up to the global Utility Service. Only if the global service cannot provide a result, an error is returned. For more details about local and global components see “Global versus Local” below.
Adapters could be considered the “glue components” of the architecture, since they connect one interface with another and allow various advanced programming techniques such as Aspect-Oriented Programming. An adapter takes a component implementing one interface and uses this object to provide another interface.
This allows the developer to split up the functionality into small API pieces and keep the functionality manageable. For example, one could write an adapter that allows an IExample content component to be represented as a file in FTP (see diagram above). This can be done by implementing the IReadFile and IWriteFile interface for the content component. Instead of adding this functionality directly to the SimpleExample class by implementing the interfaces in the class, we create an adapter that adapts IExample to IReadFile and IWriteFile. Once the adapter is registered for both interfaces (usually through ZCML), it can be used as follows:
The getAdapter() method finds an adapter that maps any of the interfaces that are implemented by example ( SimpleExample instance) to IReadFile. An optional argument named context can be passed as a keyword argument, specifying the place to look for the adapter. None causes the system to look only for global adapters. The default is the site the request was made from.
In this particular case we adapted from one interface to another. But adapters can also adapt from several interface to another. These are known as multi-adapters. While multi-adapters were first thought of as unnecessary, they are now used in a wide range of applications.
The best side effect of adapters is that it was not necessary to touch the original implementation SimpleExample at all. This means that I can use any Python product in Zope 3 by integrating it using adapters and ZCML.
Utilities are similar to services, but do not provide vital functionality, so applications should not be broken if utilities are missing. This statement should be clarified by an example.
In pre-alpha development of Zope 3, SQL Connections to various relational databases were managed by a service. The SQL Connection Service would manage SQL connections and the user could then ask the service for SQL connections by name. If a connection was not available, then the service would give a negative answer. Then we realized the role of utilities, and we were able to rid ourselves of the SQL Connection Service and implement SQL connections as utilities. Now we can ask the Utility Service to give us an object implementing ISQLConnection and having a specified name. We realized that many services that merely acted as registries could be thrown out and the objects they managed became utilities. This greatly reduced the number of services and the complexity of the system. The lesson here is that before you develop a service, evaluate whether it would just act as a container, in which case the functionality is better implemented using utilities.
Factories, as the name suggests, exist merely to create other components and objects. Factories can be methods, classes or even instances that are callable. The developer only encounters them directly when dealing with content objects (since ZCML creates factories for you automatically) if you specify the factory directive. The functionality and usefulness of factories is best described by an example.
Let’s consider our SimpleExample content component once more. A factory has to provide two methods. The obvious one is the __call__() method that creates and returns a SimpleExample instance. The second method, called getInterfaces() returns a list of interfaces that the object created by calling __call__ will provide.
Factories are simply named utilities that provide IFactory. Using a special sub-directive you can register a factory using an id, such as example.SimpleExample. Once the factory is registered, you can use the component architecture’s createObject() method to create Simple Examples using the factory (implicitly):
The argument is simply the factory id. By the way, a factory id must be unique in the entire system and the low-level functionality of factories is mostly hidden by high-level configuration. Optionally you can specify a context argument, that specifies the location you are looking in. By default it is the site of the request; if you specify None, only global factories are considered.
Presentation components, especially views, are very similar to adapters, except that they take additional parameters like layers and skins into account. In fact, in future versions of Zope 3, the presentation service will be removed and presentation components will become adapters. Presentation components are used for providing presentation for other components in various output formats (presentation types), such as HTML, FTP, XML-RPC and so on.
In order to make a view work, two pieces of information have to be provided. First, the view must know for which object it is providing a view. This object is commonly known as the context of the view. Second, we need to know some protocol-specific information, which is stored in a Request object that is always accessible under the variable name request in the view. For HTML, for example, the request contains all cookies, form variables and HTTP header values, but also the authenticated user and the applicable locale. The return values of the methods of a view depend on the presentation type and the method itself. For example, HTTP views usually return the HTML body, whereas FTP might return a list of “filenames”.
Resources are presentation components in their own right. In comparison to views, they do not provide a presentation of another component but provide some presentation output that is independent of any other component. HTML is a prime example. Stylesheets and images (layout) are often not considered content and also do not depend on any content object, yet they are presentation for the HTTP presentation type. However, not all presentation types require resources; both FTP and XML-RPC do not have such independent presentation components.
Next, views and resources are grouped by layers, which are usually used for grouping similar look and feel. In Zope 2’s CMF framework, layers are the folders contained by the portal_skins tool. An example for a layer is debug, which simply inserts Python tracebacks into exception HTML pages.
Multiple layers can then be stacked together to skins. Currently we have several skins in the Zope 3 core: “rotterdam” (default), “Basic”, “Debug”, and “ZopeTop” (excluded from the 3.0 distribution). The skin can simply be changed by typing ++skin++SKINNAME after the root of your URL, for example:
When you develop an end-user Web site, you definitely want to create your own layer and incorporate it as a new skin. You want to avoid writing views for your site that each enforce the look and feel. Instead you can use skins to create a look and feel that will work for all new and existing templates.
Zope 3 separates consciously between local and global components. Global components have no place associated with them and are therefore always reachable and present. They are always initialized and registered at Zope 3 startup via the Zope Configuration Markup Language (ZCML), which I mentioned before. Therefore, global components are not persistent meaning they are not stored in the ZODB at all. Their state is destroyed (and should be) upon server shutdown.
Local components, on the other hand, are stored in the ZODB at the place in the object tree they were defined in. Local components always only add and overwrite previous settings; they can never remove existing ones. Creating new local components can be done using the Web Interface by clicking “Manage Site”. This will lead you into the configuration namespace and is always marked by ++etc++site in the URL.
Every folder can be promoted to become a site. Once a local site manager is declared for a folder by clicking on “Make Site”, we call this object/folder a site.
As mentioned before, local components use an explicit acquisition process when looking up information. For example, I want to get the factory for SimpleExample.
When looking for any component, the original site of the publication is chosen. However, sometimes it is desired to start looking for a component from a different site. In these cases you simply specify the context in the call.
If a local Utility Service exists and an IFactory utility with the name example.SimpleExample is found, then it is returned. If not, then the local Utility Service delegates the request to the next site. Requests can be delegated all the way up to the global Utility Service at which point an answer must be given. If the global Utility Service does not know the answer either, a ComponentLookupError is raised.
We can see that there are slight semantic differences between global and local implementations of a service, besides the difference in data storage and accessibility. The global service never has to worry about place or the delegation of the request. The net effect is that global components are often easier to implement than their local equivalent. Furthermore, local components usually have writable APIs in addition to the readable ones, since they have to allow run-time management.