Plone使用Adapter实现Aspect-oriented 编程
Plone使用Adapter实现Aspect-oriented 编程
http://www.315ok.org/blogfolder/104
http://www.315ok.org/logo.png
Plone使用Adapter实现Aspect-oriented 编程
Plone使用Adapter实现Aspect-oriented 编程
When modeling design with interfaces, we should always endeavor to adhere to the principle of separation of concerns. A component, as described by an interface, should do one thing and one thing only, providing the minimum necessary operations (methods) and attributes (properties) to support that function.
In complex systems such as Plone, we often need to provide general functionality that can act on different types of objects. Continuing with our earlier examples,consider an instrument that is playable.
Zope 2 suffers from all of these problems. Just take a look at the number of base classes on a typical content item, using the Doc tab in the ZMI. These support various aspects of content items, such as local role support, persistent properties, or WebDAV publishing.
With Zope 3 programming techniques, different aspects of various objects are provided by different adapters, adapting the object to a different interface. For example:
We now have a means of turning any IGuitar into an IPlayable. Before we can use the adapter, however, we must register it using ZCML:
A more specific adapter is one registered for a more specific interface. For example, an adapter matching an interface directly implemented by the object's class is more specific than one matching an interface implemented by a base class, a parent interface of an interface provided by the object. Although this sounds complicated,the Component Architecture tends to find the adapter you would expect it to find,given a number of general and specific adapter registrations.
Let us look at an example. If we tried to adapt the pbass object, we would get the same general IGuitar adapter:
It marks working copies with an IWorkingCopy marker interface when they are checked out and uses various adapters registered for this interface to override more general adapters that apply to base copies. When a working copy is checked back in again, the marker interface is removed (using noLongerProvides() from zope.interface), and the behavior reverts to normal.
The most general adapter is one registered for Interface. This can sometimes be useful when constructing global fallback adapters. In ZCML, we can express this with:
[size=5]Multi-adapters[/size]
So far, we have seen adapters that vary by a single interface, adapting a single context. It is also possible to register adapters that adapt multiple objects, and thus can be specialized on any one or more of their interfaces.Suppose we were dealing not only with guitars, but also with amplifiers:
Multi-adapters are a little less common than regular adapters. If you have an adapter where most methods take the same parameter, it is normally a sign that you really want a multi-adapter. Being able to specialize based on multiple dimensions (i.e. the different interfaces being adapted) can add a lot of flexibility, possibly at the cost of additional complexity. Internally in Zope, multi-adapters are used all the time—more on that when we get to views in a moment.
[size=5]Named Adapters[/size]
Like utilities, adapters can be named, with unnamed adapters really just being named adapters called u"". Named single-adapters are not particularly common, but can make sense if behavior needs to vary not just based on the type of object being adapted, but also based on user input or other run-time configuration. Suppose that we wanted to let the user pick the style in which a guitar was played.
In the example above, we are doing something a little different to what we did in earlier examples—using a function that returns an object as the adapter factory,rather than a class. Zope only requires that factories be callables that take the appropriate number of parameters and return an object providing the desired interface. The @adapter and @implementer function decorators are analogous to
using adapts() and implements() for a class.
This pattern can also be useful if you want to return an adapter that is not a class referencing the adapted object. For example, in plone.contentrules, there is an adapter factory that allows constructs like:
In complex systems such as Plone, we often need to provide general functionality that can act on different types of objects. Continuing with our earlier examples,consider an instrument that is playable.
>>> class IPlayable(Interface): ... """An instrument that can be played ... """ ... ... def __call__(tune): ... """Play that tune! ... """We may write some general code that expects an IPlayable. An object-oriented programming approach could be to use a mix-in or base class and relying on polymorphism:
>>> class PlayableMixin(object): .... implements(IPlayable) ... def __call__(self, tune): ... print "Strumming along to", tune >>> class BassGuitar(PlayableMixin): ... pass >>> class ClassicalGuitar(PlayableMixin): ... passThis will work. However, with multiple aspects of instruments in general, and guitars in particular to model, we would quickly end up with a large number of mix-in classes, bloating the APIs of the sub-classes, and incurring the risk of naming conflicts. Furthermore, if we needed to model some new aspect of an instrument,we could end up having to modify a several classes to use a new mix-in. By tightly weaving a number of application-specific classes into the inheritance hierarchy, this approach also makes re-use much more difficult.
Zope 2 suffers from all of these problems. Just take a look at the number of base classes on a typical content item, using the Doc tab in the ZMI. These support various aspects of content items, such as local role support, persistent properties, or WebDAV publishing.
With Zope 3 programming techniques, different aspects of various objects are provided by different adapters, adapting the object to a different interface. For example:
>>> from zope.interface import Interface, Attribute
>>> class IGuitar(Interface):
... """A guitar
... """
...
... strings = Attribute("Number of strings")
>>> class IBass(IGuitar):
... """A bass guitar
... """
>>> class IElectric(IGuitar):
... """An electric guitar
... """
>>> from zope.interface import implements
>>> class Bass(object):
... implements(IBass)
... strings = 4
>>> class Electric(object):
... implements(IElectric)
... strings = 6
>>> pbass = Bass()
>>> tele = Electric()
Here, we are explicitly modeling different types of guitars. We will make use of this level of granularity later, but let us first provide a simple adapter from IGuitar to IPlayable:>>> from zope.component import adapts >>> class GuitarPlayer(object): ... implements(IPlayable) ... adapts(IGuitar) ... ... def __init__(self, context): ... self.context = context ... ... def __call__(self, tune): ... print "Strumming along to", tuneThe __init__() method takes a parameter conventionally called context. This is the object being adapted, in this case an IGuitar. The adapter itself provides IPlayable, and fully implements this interface by defining a __call__() method.
We now have a means of turning any IGuitar into an IPlayable. Before we can use the adapter, however, we must register it using ZCML:
<adapter factory=".players.GuitarPlayer" />This shorthand version inspects the class for implements() and adapts() declarations. To be more explicit, or in case these were omitted or ambiguous, we could use:
<adapter
provides=".interfaces.IPlayable"
for=".interfaces.IGuitar"
factory=".players.GuitarPlayer"
/>
The simplest way of looking up an adapter is by calling the interface we want to get an adapter to:>>> tele_player = IPlayable(tele)
>>> tele_player("Toxic Girl")
Strumming along to Toxic Girl
When the Component Architecture is looking for an appropriate adapter from the tele object to an IPlayable, it performs a search of the registered adapters against the interfaces provided by the context object (tele). If the object provides the desired interface itself, it will be returned as-is (known as a null-adapter). Otherwise, the most specific adapter available will be instantiated and returned.A more specific adapter is one registered for a more specific interface. For example, an adapter matching an interface directly implemented by the object's class is more specific than one matching an interface implemented by a base class, a parent interface of an interface provided by the object. Although this sounds complicated,the Component Architecture tends to find the adapter you would expect it to find,given a number of general and specific adapter registrations.
Let us look at an example. If we tried to adapt the pbass object, we would get the same general IGuitar adapter:
>>> pbass_player = IPlayable(pbass)
>>> pbass_player("Como Ves")
Strumming along to Como Ves
We could register a more specific adapter for IBass, however:>>> class BassPlayer(object): ... implements(IPlayable) ... adapts(IBass) ... ... def __init__(self, context): ... self.context = context ... ... def __call__(self, tune): ... print "Slappin' it to",And in ZCML:
<adapter factory=".players.BassPlayer" />Now, we will get the new, more specific adapter for pbass, but not for tele:
>>> tele_player = IPlayable(tele)
>>> tele_player("Toxic Girl")
Strumming along to Toxic Girl
>>> pbass_player = IPlayable(pbass)
>>> pbass_player("Como Ves")
Slappin' it to Como Ves
This is a very powerful concept. For example, imagine that Plone comes with some standard functionality, written as an adapter for, say, Products.CMFCore.interfaces.IContentish, which applies to most if not all content items. Content types with particular needs can then provide a more specific adapter by registering it for a more specific interface. In general, if code is written to look up adapters when working with a particular aspect of an object, it will be extensible in this way.Furthermore, recall that specific objects can be marked with an interface using alsoProvides(). An interface that is provided directly by an object is more specific still than one implemented by its class. Therefore, we could register an adapter for an interface that is conditionally applied to objects, and expect conditional behavior accordingly. Plone's staging solution, plone.app.iterate, uses this technique.It marks working copies with an IWorkingCopy marker interface when they are checked out and uses various adapters registered for this interface to override more general adapters that apply to base copies. When a working copy is checked back in again, the marker interface is removed (using noLongerProvides() from zope.interface), and the behavior reverts to normal.
The most general adapter is one registered for Interface. This can sometimes be useful when constructing global fallback adapters. In ZCML, we can express this with:
<adapter
for="*"
provides=".interfaces.IPlayable"
factory=".players.FallbackPlayer"
/>
With no such general fallback, we would get a TypeError when trying to look up an adapter for which no registration is found. This can happen legitimately if, for example, we are depending on other packages to provide appropriate adapters, or if some aspect of an object is deemed optional. We can write more defensive code by using:>>> possibly_playable = IPlayable(some_object, None)If no adapter is found, this will return None, or whatever else is passed as the second parameter.
[size=5]Multi-adapters[/size]
So far, we have seen adapters that vary by a single interface, adapting a single context. It is also possible to register adapters that adapt multiple objects, and thus can be specialized on any one or more of their interfaces.Suppose we were dealing not only with guitars, but also with amplifiers:
>>> class IAmp(Interface):
... """An amplifier
... """
...
... goes_up_to = Attribute("How far up does it go?")
>>> class ElevenAmp(object):
... implements(IAmp)
... goes_up_to = 11 # This one goes to eleven!
>>> vox = ElevenAmp()
To do a gig, we would need both a guitar and an appropriate amp. We will model this by adapting the guitar and the amp to an IGiggable interface. Notice how the __init__() method now takes two parameters, since there are two objects being adapted:>>> class IGiggable(Interface): ... """A setup which can be gigged ... """ ... ... def __call__(stage_set): ... """Gig a particular set ... """ >>> class GigRig(object): ... implements(IGiggable) ... adapts(IElectric, IAmp) ... ... def __init__(self, guitar, amp): ... self.guitar = guitar ... self.amp = amp ... ... def __call__(self, stage_set): ... print "Setting volume to", self.amp.goes_up_to ... playable = IPlayable(self.guitar) ... for song in stage_set: ... playable(song)To register this adapter, we use the same ZCML directive as before:
<adapter factory=".gig.GigRig" />If we omitted the adapts() declaration, we would need to specify the two adapted interfaces in the for attribute, separated by whitespace:
<adapter
provides=".interfaces.IGiggable"
for=".interfaces.IElectric .interfaces.IAmp"
factory=".gig.GigRig"
/>
To look up a multi-adapter, we cannot use an interface on its own, since that only takes a single context parameter. Instead, we do:>>> from zope.component import getMultiAdapter >>> gig = getMultiAdapter((tele, vox,), IGiggable) >>> gig(["Foxxy Lady", "Voodoo Chile",]) Setting volume to 11 Strumming along to Foxxy Lady Strumming along to Voodoo ChileThere is also zope.component.queryMultiAdapter, which will return None if the adapter lookup fails.
Multi-adapters are a little less common than regular adapters. If you have an adapter where most methods take the same parameter, it is normally a sign that you really want a multi-adapter. Being able to specialize based on multiple dimensions (i.e. the different interfaces being adapted) can add a lot of flexibility, possibly at the cost of additional complexity. Internally in Zope, multi-adapters are used all the time—more on that when we get to views in a moment.
[size=5]Named Adapters[/size]
Like utilities, adapters can be named, with unnamed adapters really just being named adapters called u"". Named single-adapters are not particularly common, but can make sense if behavior needs to vary not just based on the type of object being adapted, but also based on user input or other run-time configuration. Suppose that we wanted to let the user pick the style in which a guitar was played.
>>> class StyledGuitarPlayer(object): ... implements(IPlayable) ... ... def __init__(self, context, style): ... self.context = context ... self.style = style ... ... def __call__(self, tune): ... print self.style, "to", tune >>> from zope.component import adapter >>> from zope.interface import implementer >>> @implementer(IPlayable) ... @adapter(IGuitar) ... def fingerpicked_guitar(context): ... return StyledGuitarPlayer(context, 'Picking away') >>> @implementer(IPlayable) ... @adapter(IGuitar) ... def strummed_guitar(context): ... return StyledGuitarPlayer(context, 'Strumming away')And in ZCML:
<adapter
factory=".styles.fingerpicked_guitar"
name="fingerpick"
/>
<adapter
factory=".styles.strummed_guitar"
name="strum"
/>
To look up a named adapter, we need to use getAdapter() or queryAdapter(),like this:>>> from zope.component import getAdapter
>>> preferred_style = u"fingerpick"
>>> playable = getAdapter(tele, IPlayable, name=preferred_style)
>>> playable("Like a Hurricane")
Picking away to Like a Hurricane
[size=5]Adapter Factories[/size]In the example above, we are doing something a little different to what we did in earlier examples—using a function that returns an object as the adapter factory,rather than a class. Zope only requires that factories be callables that take the appropriate number of parameters and return an object providing the desired interface. The @adapter and @implementer function decorators are analogous to
using adapts() and implements() for a class.
This pattern can also be useful if you want to return an adapter that is not a class referencing the adapted object. For example, in plone.contentrules, there is an adapter factory that allows constructs like:
assignable = IRuleAssignmentManager(context) assignable['key'] = assignmentHere, the context could be a content object, and assignable is a container object that stores assignments of rules to that context. The adapter factory, which can be found in plone.contentrules.engine.assignments, retrieves a persistent instance of the container that is stored in an annotation on the context.
An annotation is a general way to store additional metadata on an object,using a dictionary-like syntax. See zope.annotation.interfaces for more.The calling code, of course, does not care where the adapter came from, only that it correctly implements IRuleAssignmentManager and pertains to the particular context.