[PEAK] Jumping Aspects
Phillip J. Eby
pje at telecommunity.com
Sat Jul 31 01:44:29 EDT 2004
No, it's not a new saying of Batman's sidekick (Holy jumping aspects,
Batman!), it's a tricky design problem in creating interceptors for the new
workspace-based system.
I'd like for multiple schema components to be able to contribute behavior
to a workspace's customized classes. Mostly, this will consist of putting
various kinds of wrappings around methods, either on a model class, or on
one of its features. So, there needs to be a sensible way to order these
wrappings with respect to one another.
However, some domain features or methods are defined in terms of *other*
features or methods. For a simple example, multi-valued features usually
offer methods like 'add()', 'remove()', 'replace()' and so on. These
methods are implemented in terms of the "link" and "unlink" primitives
which add or remove a single reference.
In principle, we could simply put wrappers around the "link" and "unlink"
primitives, and use them to drive any storage methods or validation events
or UI notifications. But this leads to potential performance and other
issues. For example, given the way features currently work, simply
assigning *the same list* to an attribute could result in perhaps hundreds
of link/unlink events being generated. If the events are being used to
drive a UI, this could be disastrous from a user-experience point of
view. If the events are being used to update a database, it could be
equally problematic, even if the updates are batched.
In general, then, we would like to be able to intercept a higher-level
operation, without having to intercept every one of its lower-level
components. But, if a lower-level operation is invoked directly, we still
need our lower-level interception to occur. This is known in the AOP
literature as a "jumping aspect": one where the desired point of
interception can vary depending on the system's control flow.
One of the classic solutions to this issue can be found in GUI frameworks,
where there is usually some mechanism to suppress screen updates
temporarily, preventing the screen from "thrashing" as a bunch of low-level
events occur. In essence, one uses a flag. This approach works nicely
when there are a limited number of things needing flags.
For our purposes, however, the situation is a bit more complex. Each
schema component that wants to modify the behavior of some high-level
operation needs its own per-object flag to indicate whether lower-level
operations should also be intercepted, or else there needs to be some sort
of stack-tracing mechanism to check where the operation was called
from. Both approaches seem complex, to say the least.
One approach from the AOP literature is to use a proxy to add
wrappers. The wrapping occurs in the proxy's methods, and those methods
then call down to the "real" object. When a method is defined in terms of
another, calls made from inside the "real" object's code will call
unwrapped "real" methods. Thus, already-wrapped methods will not be called
again from inside the object.
Unfortunately, this doesn't help with the situation where code in one of
those methods may "call out" to other objects, passing the "real"
object. It also doesn't address the problem of multiple schema components
contributing wrappers. For example, a DB aspect that's implemented in
low-level calls, *and* a UI aspect that's implemented in high-level
calls. Thus, the simple proxy approach does not support composable
aspects, at least not without creating a chain of proxies.
Consider an interface with methods A and B, where A is implemented by
calling B. If there are two aspects that want to advise these methods, and
aspect 1 has advice for both A and B, and aspect 2 only has advice for B,
then this can be implemented with two proxies. The first proxy wraps both
A and B with aspect 1's advice, and method B with aspect 2's
advice. Method A of the first proxy calls method A on the second
proxy. Method B of the first proxy calls method B on the original object,
but on the second proxy it first implements aspect 2's advice.
The net result of this arrangement is that as long as an overall ordering
of aspects' advice can be seen to exist, it can be implemented using a
number of proxies that is between 1 and the total number of aspects
advising. Unless... what if we just changed the object's class on-the-fly?
In essence, we would be creating a "state machine" of classes for each
model class. In the simplest case, there would be only one: the one you
normally get from the workspace. However, if there are aspects (schema
components) that intercept subsets of other aspects' interceptions, then
there would be additional classes.
It seems sort of weird, but basically what would happen is that if you
called someObject.setFoo(42), then there would be some functions getting
called to do DB-specific or UI operations, following which the __class__ of
'someObject' would change to the abstract domain class, and the original
"abstract" setFoo(42) would execute. Finally, the class would return to
being the workspace-specific class.
I'm starting to like this, though. It doesn't require a bunch of flags on
the object, and it doesn't even use any *space* in the object that wasn't
already needed. The logic required to construct the classes is a bit
complex, though. And I'm not sure that you can always construct them in
the general case. It seems to me that there must be cases that would
result in conflict.
Well, let's work it out and see. We essentially want to say that a given
piece of advice applies to some method M, possibly excluding calls nested
from some set of other methods that make the advice moot. The starting
class includes all advice, indicating a state where no exclusions are
possible because no methods have been called. For each method, we can then
construct a follow-up state after removing all the advice that no longer
applies (due to exclusion by that method). Thus for any state and method,
the follow-up state will have a smaller amount of applicable
advice. Therefore, the generation of states will eventually terminate with
either the zero-advice state (the original class), or a state consisting of
all the methods which are advised regardless of calling context. So,
although it's not always possible to create a purely linear organization of
classes, the required state machine is always finite.
It would appear, however, that I made a mistake in my earlier supposition
that the number of proxy states/classes would max out at the number of
schema components offering advice. It would appear instead that the
theoretical maximum number of states is in fact quite large, and the
maximum number that can be passed through in sequence is actually the same
as the number of methods which have *any* advice applied to them.
But, I don't think this is a big worry in practice. Memoizing the class
generation should prevent redundant classes from being generated, and most
domain objects should have relatively shallow hierarchies of
self-delegation, making the state machine relatively simple. If additional
speed is needed, it probably also won't be that complicated to add a C
implementation of the class-swapping code.
Oh crap. I just realized something. This arrangement only handles
"jumping aspects" that apply to a *single object*. It does nothing for
jumping across objects. For example, if some domain operation on a
"Customer" object involves lots of changes to "Invoice" objects, there's no
way to deal with that automatically in this model.
Ah well. I think the need for such interceptions is going to be relatively
infrequent in the system as a whole, and there are several ways to tackle
them manually when they do arise (such as temporarily changing each
Invoice's class as it's processed!). Anyway, I think that at least 80-90%
of the "jumping aspects" in PEAK will revolve around feature-level
modifications to objects, not domain-specific methods. So, if these simple
things are taken care of, and complex things are still possible, I think
we're probably okay.
The only other problem is that technically speaking, some of the methods
we're intercepting aren't on the class itself, but on features of the
class. This makes the class generation process a lot hairier, since we
have to generate fresh features wherever the advice occurs. On the bright
side, we can use unadvised features without modification from the original
class.
Or can we? What if the advice is some type of class replacement? What
about non-method advice? Or should we limit all advice to behavioral
triggers? I don't see that as a real limitation, so if it simplifies
implementation I think we should go ahead with it. It does bring to mind,
however, that features have lots of bindings whose values are determined on
a per-class basis, and thus subclassing features for purposes of creating
"advised" versions is therefore going to be tricky. (For example,
'feature.typeObject' and the various convenience methods that delegate to it.)
Indeed, wherever metaclass-defined class attributes exist, there are
potential issues with subclassing as a means of generating an advised
version of the class. For example, the 'mdl_isAbstract' flag is
automatically cleared in subclasses unless it's explicitly set in the subclass.
I don't know what to do about that, exactly. The only option that will
guarantee consistency is to completely rebuild the "new" classes as clones
of the old, but not as subclasses. However, this also breaks domain-model
code that makes explicit reference to the original classes. (On the other
hand, code that does that will break under the workspace model anyway!)
I think that the only "safe" way to generate a subclass of either a feature
or element class is to create a subclass of the original, that has the same
dictionary contents that the original did *when it was created* (i.e.,
leaving out subsequent setattrs or computed bindings). While I can
envision metaclass hierarchies where this would turn out badly, I don't
think peak.model's stuff falls into that category. At any rate, I think
this approach should be a good place to start, although it does impose a
cost of an extra dictionary per class.
More information about the PEAK
mailing list