1.1.10 Big Example 2 -- Extending the Framework for Context

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:

``know'' it's a local protocol

know what protocol it's a local subset of

know what ``context'' object it's a local protocol for

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.

If you find the idea of
context-specific interfaces and adapters interesting, you'll find ``Object
Teams'' intriguing as well. In effect, the ideas we've presented here map onto
a subset of the ``Object Teams'' concept. Our local interfaces correspond
to their ``abstract roles'', our local adapters' instances map to their ``role
instances'', and our contexts are their ``team instances''. Adapting an object
corresponds to their ``lifting'', and so on. The main concept that's not
directly supported by our implementation here is ``callin binding''. (Callin
binding is a way of (possibly temporarily) injecting hooks into an adapted
object so that the adapter can be informed when the adapted object's methods
are called directly by other code.)