-
Transient Message Refactoring and Component Usage Philosophy
last modified November 16, 2007 by whit
Backstory
In the recent reorg, I moved a number of interfaces out of smaller tendrils of the nui codebase into the more central opencore.interfaces area. Normally this is a transparent change except for when interfaces are used in relation to persistent objects. This happens in 2 cases: marker interfaces on persistent objects, local utility registration. Marker interface can be solved with module aliases. Local utilities require a registration to what the component architecture sees as a new interface.
We have 2 major local utilities introduced in nui: IEmailInvites and ITransientMessages. The first I created a migration for. The implementation of the second had certain issue I will discuss here and I chose to refactor it as a adapter using an annotation on the portal to store it's state.
Why did I refactor vs. migrate?
The former implementation holds a potentially probablematic persistent reference to the annotation on the portal (local utilities are persisted therefore all attributes of them are also persisted). This is an IAnnotation factory that holds a reference to the portal itself. When acquisition wrapped objects are persisted, memory leaks can occur. Persisting the annotation potential could persist everything it references.
Furthermore the pattern usage looked wrong to me which is what I will discuss below.
Why did I make it an adapter?
In short, because it looked like one. It was instantiated with a portal object and required that object for every operation. In form, it looks like a classic use of IAnnotation by an adapter, rather than a utility.
And refactoring to an adapter also was very natural. Refactoring was pretty easy, since every single getUtility call also was passing in the portal as the context and overall resulted in less code and less setup steps and no need for later migration.
Well... why do you think it was made a utility in first place smarty pants?
really, I don't know (hopefully they will fill in this part for me). I think the original authors felt that the behavior of the code felt "tool like". In other words, it was a natural aesthetic choice. It seemed like it could be a hammer, so they made it a hammer.
Old school tools and new school utilities seem to exist in the same space, but closer inspection reveals some important divergence. Utilities are not a replacement for everything tools did. Utilities owe much of their existence to the lessons learned in CMF, but operate much differently.
The form factor of CMF tools and zope3 utilities are a bit different. In the old zope world, tools were the primary way to access un-protected python code on the system. In zope3, views take this role. In zope3 utilities allow direct exposure of external python modules are queryable utilities (ex our use of httplib2 in opencore) which is a more structured and specific way of interfacing with python modules (vs. importing the module into use with your view).
In cmf tools also provided general areas for persisting data about policy (an example is our teams and team memberships). In zope3, we have other mechanisms that allow use to persist data outside of the normal flow of content, primarily the broad abstraction for this is annotations. Annotations are accessed via adaptation rather than by the getUtility call and this has a number of advantages when testing and persisting data. The access to persistence stays constant, it always uses the IAnnotations adapter to access. You could store things however you like behind that abstraction and alter them however you like in front. The interface for persistence for utilities is dependent on the utility and in the case of local utilties, this means any change to the method of lookup or storage could require migration.
When should I use a utility vs. adapter? (where rob and I disagree a bit)
My rule of thumb: utilities can contribute to or be a context, but should need no context to operate. when using a utility, you shouldn't care whether it is local to a particular site or is a global utility. If a utility requires passing in a persistent (or even just another) object to instantiate or do much of it's work, it probably shouldn't be a utility but an adapter (ala ITransientMessages).
Inversely, if an adapter class does nothing with the context... it might work as a utility though the spelling is significant.
For example:
the ISomeInterface(someobject) says that whatever I get back fufills ISomeInterface by whatever means is available (the object itself or registered adapter) whereas zope.component.getUtility(ISomeInterface) says return me whatever has be registered as a utilty for ISomeInterface.
The first spelling is a primary interface; the user doesn't care how it happens, something with ISomeInterface will be returned and they can use it. The second spelling is much more specific.... you are asking for a singleton registered to ISomeInterface. ISomeInterface(someobject) might return getUtility(ISomeInterface) but never vice versa.
imho Interfaces are higher level, utilities are lower level.
My second rule of thumb: if you are using a local utility to persist data that is pertinent to a context, you might be better off using at an adapter and annotations.
In the case of transient messages and possibly the case of IEmailInvites, the data persisted is fairly specific to a context (the portal, our site). These are messages concerning the site and it's denizens, not say, the addresses of external applications or which local mailserver to use. IOW, they refer and are expected in a context (that of the portal) rather without one.
The registration of an adapter using annotations can be changed in the filesystem code with no updating or migration (as evidenced by the refactoring of ITransientMessage), whereas a local utility must be migrated (as evidenced by EmailInvites). To test utilities, you must register zcml, and do persistent changes to the zodb in your test setup. Our layer now do this, but slinkp spent a long time fishing for errors caused by forgetting to do the second part.
Generally less persistence means less headaches and IAnnotations is a good generalized storage abstraction... there is no reason to re-roll our own. The spelling is also sleeker: one import for IWhatever(context) vs. potentially 2 for getUtility(IWhatever, context=somecontext). If involvement of a utility is necessary from here until eternity, it can easily be folded into the adapter registration whereas again, the inverse is not possible.
Utilities can alway go very naturally behind interfaces but interfaces and adaptation get very tangled going the other way. In my mind, it is much like the arguments for object orientation; the interface hides the ugly bits (like fetching utilities) so programmers can concentrate on operating against the interface.
In summary
when I would use a local utility
Note the repeated use of from this point in the hiearchy
- to override a global utility for policy reasons
I want opencore.redirect to see the default url as my box name, not localhost
- to provide storage mediation behind an interface (say, store all annotation for a site in sql rather than zodb)
I want to store all the thing handled by IAnnotation in sql or an external service from this point in the hiearchy - to store policy information about externalities (this could go either way).
I want everything in the hieararchy from here down to talk to this cab instance not that one
(and for clarity) when I would use a utility
- configuration or policy information
storing data from zcml, external address, etc
- structured access to python code
our use of httplib2 as a utility - sometime mediation with external processes
#2 again is an example, or a more specific client - general contextless support for other code
i18n
I would put utilities behind an adapter whenever the utility needs to interact with some context (yeah.. I know this is essentially what our views do since they are adapters themselve, but there is alot of long ugly code in our views that could fall behind an iface).
Adapters and utilities are simply useful abstractions for making things a bit easier to deal with, just like functions and objects. If something seems hairy and exposed or difficult to reuse, they can come in handy. The primary entity here is the interfaces, next single adaptation and specific cases of multiadaptation, then utilities and subscribers, and sitemanagers.