Interfaces
If you were trying to understand b-org without a comprehensive tutorial to hand, you would do well to look at the interfaces package. You will notice that this is subdivided into various files
- interfaces/department.py
- Contains a description of a department (IDepartment) and a marker interface for the content object that stores the department (IDepartmentContent).
- interfaces/employee.py
- Contains the equivalent interfaces, IEmployee and IEmployeeContent, as well as the definition of a specific event interface, IEmployeeModified.
- interfaces/project.py
- Again contains IProject and IProjectContent, as well ILocalWorkflowSelection, which is used to denote a utility that defines the placeful workflow policy that projects will use.
- interfaces/workspace.py
- Holds the interface IWorkspace, which is used by the local-role PAS plug-in to extract which users should have which local roles in a project.
- interfaces/schema.py
- Contains interfaces relevant to the custom schema extension mechanism - ISchemaExtender, IExtensibleSchemaProvider and ISchemaInvalidatedEvent.
- interfaces/utils.py
- Defines interfaces that are used as input to various vocabularies - IEmployeeLocator, IAddableTypesProvider and IValidRolesProvider.
In order to understand what each of these interfaces describes in more detail, look at the files above. Recall that interfaces are mainly documentation - these interfaces are accompanied by docstrings and generally self-documenting code.
The various interfaces intended for public consumption are imported to interfaces/__init__.py, so that client code can write, e.g.:
from Products.borg.interfaces import IEmployeeThis is a common idiom. If you find yourself with too many interfaces to manage in interfaces/__init__.py, you don't necessarily need to do this, but it's probably a sign that you should be breaking your code into smaller packages!
Remember that unless you have a particular need to depend on Zope 2, then you don't need to pollute the Products namespace with such components! (and even if you do, with PythonProducts or Zope 2.10, you can do without the Products/ namespace too). For example, we could have placed the employee functionality in a package borg.employee, found in lib/python/borg/employee as a plain-python library, possibly depending on Zope 3 components (i.e. packages in the zope.* namespace).
Conversely, if you have relatively few interfaces, you can simply have an interfaces.py module without a directory.
Separating Archetypes from real components
One thing you may notice is that we have split the interface describing the concept of e.g. an employee (IEmployee) from the interface that describes the employee content object in the ZODB (IEmployeeContent). Whether this is always the right thing to do is debatable, but the reasoning goes something like this:Archetypes objects contain a very large API. Archetypes schemas and the infamous ClassGen generate methods on the content objects corresponding to schema fields, so that a field name gets an accessor called getName() and a mutator called setName(). This is all rather Archetypes-specific, and in Zope 3 schemas, we typically prefer simple properties (a name attribute) to pairs of methods. To avoid being constrained by the Archetypes when defining interfaces (Archetypes is just one implementation choice), we created IEmployee as follows:
class IEmployee(Interface):To support this, we could put the relevant properties into the Archetypes content object, but this is cumbersome, since the property() declaration normally used to convert methods to properties will only work when those methods actually exist, not when they are created by ClassGen.
"""An employee, which is also a user.
"""
id = schema.TextLine(title=u'Identifier',
description=u'An identifier for the employee',
required=True,
readonly=True)
fullname = schema.TextLine(title=u'Full name',
description=u"The employee's full name for display purposes",
required=True,
readonly=True)
Instead, we mark the content object with a marker interface, IEmployeeContent and then register an adapter to IEmployee. Strictly speaking, this is cheating, since the adapter makes assumptions about its context (such as which methods are available, and the fact that it uses Archetypes) that are not formally defined in the interface. To save excessive typing and retain some sanity in the interface definitions, it's not a terrible compromise though. Here's the adapter, from membership/employee.py:
class Employee(object):
"""Provide department information.
"""
implements(IEmployee)
adapts(IEmployeeContent)
def __init__(self, context):
self.context = context
@property
def id(self):
return self.context.getId()
@property
def fullname(self):
return self.context.Title()
Now, you can write:
emp = IEmployee(some_employee_content_object)
print emp.fullname
Another side-effect of this pattern is that we can separate things that are Archetypes-dependent from things that operate on the more general notion of an employee. For example, membrane generally makes assumptions about operating on Archetypes content objects, so the various membrane adapters adapt IEmployeeContent, whereas the view for charity employees is only concerned with "real" employees and so adapts the context to IEmployee.
This pattern is repeated for Departments and Projects as well.
Interfaces intended for utilities and adapters
Although interface design should generally not be too concerned with how those interfaces are implemented, you will often think "this is going to be used a a utility" or "this will most likely be an adapter". In this case, you may want to make some reference in the doc-string at least. For example, the ILocalWorkflowSelection interface states:class ILocalWorkflowSelection(Interface):Conversely, many interfaces are context-dependent, which means that most likely they will either be directly provided by a particular object or adaptable to it. Take the IAddableTypesProvider:
"""A selection of a local workflow for projects.
This will normally be looked up as a utility.
"""
workflowPolicy = schema.TextLine(title=u'Workflow policy identifier',
description=u'The id of the placeful workflow policy to use',
required=True,
readonly=True)
class IAddableTypesProvider(Interface):The implication here is that client code will do something like:
"""A component capable of finding addable types in a given context.
"""
availableTypes = schema.Tuple(title=u'Available types',
description=u'A list of all addable types',
value_type=schema.Object(ITypeInformation))
defaultAddableTypes = schema.Tuple(title=u'Default addable types',
description=u'A list of types to be addable by default',
value_type=schema.Object(ITypeInformation))
from Products.borg.interfaces import IAddableTypesProviderWhether IAddableTypesProvider was provided directly by the context or (more likely) provided via an adapter is not important. The only time this distinction is really useful is in the case of marker interfaces, such as IEmployeeContent:
addableTypes = IAddableTypesProvider(context).availableTypes
class IEmployeeContent(Interface):These are often checked with providedBy():
"""Marker interface for employee content objects"""
assert IEmployeeContent.providedBy(employeeContentObject)Again, the guiding principle here is separation of concerns. The aspect of a component that can provide a list of addable types (IAddableTypesProvider) is logically distinct from (and could be varied independently of) the aspect of a component that specifies it represents a project (IProject), even though it so happens that at present projects are the only time we concern ourselves with restricting addable types.
# we've got an employee, good
In the olden days, we would probably have put methods like getAvailableProjectAddableTypes() into the Project content type. Hopefully, you'll see why this is less optimal than having it in a separate component (hint: what if you in your customisation of b-org wanted to be much more particular about which types were addable?). You will hopefully start to pick up "fat" interfaces during interface design - if you had a neat IProject interface that described attributes of a project that were to be saved alongside the project object, and then found a couple of methods about defining addable types that were related to one another but not so much to the data of a project in general, you would hopefully reach for a new interface. If so - well done, you're getting there.