Now it's time for our second ``big'' example. This time, we're going to add an extension to the protocols framework to support ``contextual adaptation''. The tools we've covered so far are probably adequate to support 80-90% of situations requiring adaptation. But, they are essentially global in nature: only one adapter path is allowed between any two points. What if we need to define a different adaptation in a specific context?
For example, let's take the documentation framework we began designing in section 1.1.1. Suppose we'd like, for the duration of a single documentation run, to replace the factory that adapts from FunctionType to IDocumentable? For example, we might like to do this so that functions used by our ``finite state machine'' objects as ``transitions'' are documented differently than regular functions.
Using only the tools described so far, we can't do this if IDocumentable is a single object. The framework that registered the FunctionAsDocumentable adapter effectively ensured that we cannot replace that adapter with another, since it is already the shortest adapter path. What can we do?
In section 1.1.7, we discussed how we could create ``subset'' protocols and ``inherit'' adapter declarations from existing protocols. In this way, we could create a new subset protocol of IDocumentable, and then register our context-specific adapters with that subset. These subset protocols are just as fast as the original protocols in looking up adapters, so there's no performance penalty.
But who creates the subset protocol? The client or the framework? And how do we get the framework to use our subset instead of its built-in IDocumentable protocol?
To answer these questions, we will create an extension to the protocols framework that makes it easy for frameworks to manage ``contextual'' or ``local'' protocols. Then, framework creators will have a straightforward way to support context-specific adapter overrides.
As before, we'll start by envisioning our ideal situation. Let's assume that our documentation tools are object-based. That is, we instantiate a ``documentation set'' or ``documentation run'' object in order to generate documentation. How do we want to register adapters? Well, we could have the framework add a bunch of methods to do this, but it seems more straightforward to simply supply the interfaces as attributes of the ``documentation set'' or ``documentation run'' object, e.g.:
So, instead of importing the interface, we access it as an attribute of some relevant ``context'' object, and declare adapters for it. Anything we don't declare a ``local'' adapter for, will use the adapters declared for the underlying ``global'' protocol.
Naturally, the framework author could implement this by writing code in the DocSet class' __init__ method, to create the new ``local'' protocol and register it as a subset of the ``global'' IDocumentable interface. But that would be time-consuming and error prone, and therefore discourage the use of such ``local'' protocols.
Again, let's consider what our ideal situation would be. The author of the DocSet class should be able to do something like:
Our hypothetical subsetPerInstance class would be a descriptor that
did all the work needed to provide a ``localized'' version of each interface
for each instance of DocSet. Code in the DocSet class would
always refer to self.IDocumentable
or self.ISignature
, rather
than using the ``global'' versions of the interfaces. Thus, we can now
register adapters that are unique to a specific DocSet, but still use
any globally declared adapters as defaults.
Okay, so that's our hypothetical ideal. How do we implement it? I personally like to try writing the ideal thing, to find out what other pieces are needed. So let's start with writing the subsetPerInstance descriptor, since that's really the only piece we know we need so far.
Whew. Most of the complexity above comes from the need for the descriptor to know its ``name'' in the containing class. As written, it will guess its name to be the name of the wrapped interface, if available. It can also detect some potential aliasing/renaming issues that could occur. The actual work of the descriptor occurs in just two lines, buried deep in the middle of the __get__ method.
As written, it's a handy enough tool. We could leave things where they are right now and still get the job done. But that would hardly be an example of extending the framework, since we didn't even subclass anything!
So let's add another feature. As it sits, our descriptor should work with both
old and new-style classes, automatically generating one subset protocol for
each instance of its containing class. But, the subset protocol doesn't
know it's a subset protocol, or of what context. If we were to print
DocSet().IDocumentable
, we'd just get something like
<protocols.interfaces.Protocol instance at 0x00ABA220>.
Here's what we'd like it to do instead. We'd like it to say something like LocalProtocol(<class 'IDocumentable'>, <DocSet instance at 0x00AD9FB0>). That is, we want the local protocol to:
What does this do for us? Aside from debugging, it gives us a chance to find related interfaces, or access methods or data available from the context.
So, let's create a LocalProtocol class:
And now, we can replace these two lines in our earlier __get__ method:
with this one:
Thus, the new local protocol will know its context is the instance it was retrieved from.
Of course, to make this new extension really robust, we would need to add some more documentation. For example, it might be good to add an ILocalProtocol interface that documents what local protocols do. Context-sensitive adapters would then be able to verify whether they are working with a local protocol or a global one. Framework developers would also want to document what local interfaces are provided by their frameworks' objects, and authors of context-sensitive adapters need to document what interface they expect their local protocols' context attribute to supply! Also, see below for a web site with some interesting papers on patterns for using localized adaptation of this kind.
Note: In practice, the idea of having local protocols turned out to be useful enough that as of version 0.9.1, our LocalProtocol example class was added to the protocols package as protocols.Variation. So, if you want to make use of the idea, you don't need to type in the source or write your own any more.
See Also: