Aggregated aspect customisation
A pattern to consider?
I little while ago, I wrote some plugins for Trac 0.11. It used a pattern not dissimilar to way we use adapters in Zope for customisation/plugins, but with a slightly different twist. Whilst not as architecturally clean, it did make some things easier. I'm wondering if we should consider using this type of pattern in Zope/Plone. At least it'd be interesting to think about.
Basically, the pattern works on the principle that the platform provides a number of interfaces that you can use to provide different types of behaviour. Each typically defines one or two methods that you have to implement. Examples - all fictional - could be:
- An INavigationElement interface with a method getNavigationElement() that returns a list of items to add to the navigation.
- An INameChooser with a method chooseName() is called to choose a name for a new content object.
- An IRoleProvider with a method getRoles() that returns the roles for the current user in context.
There would be a well documented list of such interfaces. The framework will look up all components (e.g. using named adapters, allowing you override by name, but generally just being additive) applicable to the current context that provide a particular interface, call the relevant method and use the result.
The idea here is that you build a class that describes the behaviour you want, e.g. for a particular type of content object, or for your particular Plone site (for non-contextual aspects). You build this class up, adding more interfaces and implementing the corresponding method as necessary. There's nothing to stop you from splitting your code up into multiple classes, but keeping things together normally makes things easier, at least for small sites. You stop thinking about these so much as separate adapters and more as a description of the aspects or behaviours you'd like to implement for your type.
In pseudo-code, this could look like:
class MyAspects(ContentAspects): implements(INavigationElement, INameChooser, IRoleProvider) adapts(IMyContent) def getNavigationElement(self): return ... def chooseName(self): return ... def getRoles(self): return ...
The ContentAspects base class would make sure that this class acted as an adapter, with self.context being the adapter context. The type of context is listed with adapts(), as normal, and the provided aspects are listed with implements(). The name of the adapter could default to the dotted name of the class, though you could obviously specify a different name in the registration (e.g. to override an existing set of aspects). The ContentAspects base class could also be used by a grokker to make this class auto-register the aspects without the need for ZCML. It'd need to register one (named) adapter per provided interface, all using this class as the factory.
The framework code exposing this pattern could look something like:
# build navigation for name, nav in getAdapters(context, INavigationElement): element = nav.getNavigationElement() ...
Note that to the framework code, this is an adapter like any other.
There is something a bit dirty about this. The separation of concerns in the MyAspects class is fuzzy. To get proper separation of concerns, you'd have one class/adapter per aspect, which is possibly better (certainly more granular), although it's more code to manage.
Even if we don't want to do amalgamated aspects in one class, I think the idea of thinking about some adapters as providing "aspects" holds some merit. We do this already, but we don't always think of it like that. There's a relatively manageable number of interfaces that make sense for people to create adapters for. If we listed and grouped those, we'd have a pretty good starting point for an "API" that people could browse and use to affect the behaviour of content objects and sites in Plone.