AOPTutorial
Doing AOP With TransWarp (Under Construction)
The goal of this tutorial is to provide you with all the information and examples you need to use TransWarp's AOP tools. There are many links from this tutorial which go to more technical Wiki pages about the implementation of those tools. You don't kneed to know those implementation details, however, if your goal is simply to do aspect-oriented programming. The links are simply there if you are curious, or if you want to create your own additions to the TransWarp family of tools (in which case you will need to know more about the internal workings of the framework).
Also, as you go through the tutorial, you may find it helpful to try the examples yourself in the Python interpreter, and inspect the various aspects, classes, and objects created to learn more about how the AOP system actually works.
Introduction: Components, Class Families, and Reuse
The Goal: Black Box Reuse
For decades now, the holy grail of reusability has been the notion of the "component" - a black-box reusable piece of code. Unfortunately, almost by definition, if you're going to reuse a piece of code as a black box, you have to be able to use it without changing it. That means that the code has to have been capable from the start of doing what you want, even if it's by way of parameterizing. What good are thousands of reusable components, if none of them do precisely what you want, and you can't mix and match the pieces from each that do what you want?
Many programming approaches offer partial solutions. Inheritance lets you extend an existing class. Certain design patterns help to define good hookpoints for overriding things in subclasses, or factor out overrideable behavior into collaborator objects which can be supplied by the (re)user of the code.
But those are just partial solutions, applicable only on a class-by-class basis. A real solution for components must address not just individual classes, but groups of classes that collaborate. To be worthy of the name "component", they need to be composable, as well. That is, it should be possible to build components not only from classes, but from other components as well.
In summary, a good component architecture must meet the following "3 C's" requirements:
- Customization - Components must be customizable without changing them (otherwise, you couldn't reuse the same component for different things!)
- Collaborators - It should be possible to substitute or customize a component's collaborator components or classes.
- Composition - It should be possible to use a component as a collaborator in developing a larger component
How can we achieve such a component architecture? To meet the "Customization" requirement, it would seem that we need a way to create new versions of a component, perhaps stamping them out of a template, or having some kind of factory object which can be told what we want.
Further, if we could define partial templates, which were not sufficient to create components by themselves, but which could be combined with other partial templates, we would have the ultimate in mix-and-match customizability, composition, and collaboration.
At this point, our actual unit of reuse becomes the factory or template, rather than the component itself. The template does not change, and can be distributed and reused in as many applications as desired, but the individual components created by each (re)user can be quite different.
This sounds a lot like a class and its instances, but at a higher level. It's even different from the notion of a metaclass, because metaclasses are templates for stamping out single classes, and what we're talking about needs to stamp out entire families of classes, with arbitrary numbers of classes involved in a single instantiation.
So how could we create such a template?
The Aspect - A Template for Components
In TransWarp, an "Aspect" is a template for components. It can be complete or partial, and it can be combined with other aspects to create new aspects.
An aspect can be thought of as being like a transparency used on an overhead projector. Projecting light through the transparency creates an image. Adding additional transparencies changes the image, and the order in which they are placed can make a difference in the image as well. Many different small transparencies (or multiple copies of the same one) can be placed atop a larger transparency, which may itself be being used as part of a still larger transparency. Further, we can take the projected image and turn it into a new transparency, or capture the image as a printed photograph (a component).
In TransWarp parlance, a "component" is a class and its collaborators (which may themselves be components). To be useful, these classes must of course be instantiated. To continue our transparency analogy, this would be like using the finished photograph (component) as a guide to building the thing depicted in the photo (instance). To summarize our analogy:
- Transparency = Aspect = Component Template
- Photograph = Class Family = Component
- Thing In Photo = Class Instance = Component Instance
"Weaving" an aspect (instantiating a component) creates a brand new class family: every class in it is specifically created for that instantiation. This means that the same template can be used more than once in the same application, to produce seperate (but similar) class families if they are needed for different purposes.
In Python applications, most classes are created when the modules that define them are imported. This does not change in a TransWarp application, but the process of creating those classes involves one extra step. Let's begin with a very simple example:
from TW.Aspects import Aspect class MyAspect(Aspect): pass MyComponent = MyAspect(name="MyComponent")
In this example, we create an aspect called MyAspect
, and then
call it to instantiate a component class called MyComponent
. (We
pass in the name so that our created class will have its __name__
attribute set correctly.)
Now, if we want to create an instance of our class, we can do:
aComponent = MyComponent()
Aspects Are Not Classes
At this point, all the aspect stuff looks like meaningless overhead, just as writing a class is meaningless overhead if you have only one instance and will never reuse and customize it for different purposes. Let's look at a slightly more meaningful (although still trivial) example:
from TW.Aspects import Aspect class MyAspect(Aspect): class Thing: def printMe(self): print "Hi, I'm a", self.__class__.__name__ class Rock(Thing): pass class Dog(Thing): pass MyComponent = MyAspect(name="MyComponent")
This example creates MyComponent
again, but this time as a class
with three attributes: Thing
, Rock
, and Dog
. If we were to
type the code above into the Python interpreter, and inspect
the objects, we would see something like this:
>>> MyAspect <Aspect MyAspect at 9812672> >>> MyComponent <class TW.Aspects.MyComponent at 949150> >>>
Notice that MyAspect
is not a class, even though we created it
using a class
statement. It is an Aspect object. Let's take a
closer look:
>>> dir(MyComponent) ['Dog', 'Rock', 'Thing', '__doc__', '__module__'] >>> MyComponent.Rock <class __main__.MyComponent.Rock at 949ef0> >>> dir(MyAspect) ['__bases__', '__doc__', '__name__', 'componentBases', 'dictionaries'] >>> MyAspect.keys() ['Rock', 'Dog', 'Thing'] >>> MyAspect['Rock'] <class __main__.Rock at 95b930> >>>
Aspects Make Classes - And Class Families
Notice that MyComponent
behaves just like a standard Python class
- because it is one. MyAspect
, on the other hand, acts more like
a dictionary than a class. Also, notice that MyAspect
contains
the class __main__.Rock
(the original Rock
class), while
MyComponent
contains a new class, __main__.MyComponent.Rock
.
Each class in our original template has been stamped out to form a
new class in the output component. Let's try making another
component and see what happens:
>>> New = MyAspect(name="New") >>> New <class TW.Aspects.New at 9f5960> >>> New.Rock <class __main__.New.Rock at 9f56b0> >>>
Another component, another class. Now let's take a look at the ancestry of 'Rock':
>>> MyAspect['Rock'].__bases__ (<class __main__.Thing at 95b840>,) >>> MyComponent.Rock.__bases__ (<class __main__.MyComponent.Thing at 9498f0>,) >>> New.Rock.__bases__ (<class __main__.New.Thing at 9f4360>,) >>>
Notice that each of our Rock
classes are based on a related
Thing
class; they do not all inherit from the original Thing
class. This is important: it means that when we combine and
extend aspects, we can customize base classes as well as their
subclasses.
In other words, Aspects create entire class families, which are contained in an overall class known as the "component". Let's recap what we've seen so far.
- Aspects are created using a
class
statement which includes (directly or indirectly)Aspect
as the first base class. - Aspects can contain other classes, through nested
class
statements, nestedimport
statements, or nested assignment statements. - Aspects are not classes, but instead are a special kind of object which can be used to replicate the class structure contained in them. The new class structure produced is called a "component".
- To create a component from an aspect, call the aspect, passing in
name="desired name for the component"
. - The classes inside the component are given dotted names based on the name of the component, and they inherit from each other rather than from the template classes they were replicated from.
Customizing Components
Overlaying Aspects
Clearly, it's not of much use to stamp out virtually identical class families over and over again. We want to be able to customize them in some way. How do we do this?
If we return to our analogy of transparencies, we quickly see that if we do not actually change a transparency, the only way to produce a different image is to combine transparencies, by laying one atop another. Let's look at how to do that in Python:
class Weighing(Aspect): class Thing: weight = 0 def getWeight(self): return "%s pounds" % self.weight class Dog: weight = 50 class Rock: weight = 15 WeightyThings = (MyAspect+Weighing)(name="WeightyThings")
Now we have a new aspect, Weighing
, designed to be overlaid upon
our first aspect, MyAspect
. We overlay it by adding it to the
existing aspect, which creates a new aspect. We then call that new
aspect to create a new component, WeightyThings
. Let's take a closer
look at it:
>>> WeightyThings <class TW.Aspects.WeightyThings at 9c1d00> >>> WeightyThings.Rock <class __main__.WeightyThings.Rock at 9c1900> >>> WeightyThings.Rock.weight 15 >>> WeightyThings.Rock.printMe <unbound method WeightyThings.Thing.printMe> >>> WeightyThings.Dog.getWeight <unbound method WeightyThings.Thing.getWeight> >>> WeightyThings.Thing.printMe <unbound method WeightyThings.Thing.printMe> >>> WeightyThings.Thing.getWeight <unbound method WeightyThings.Thing.getWeight> >>>
Our new component's contained classes have their own weight
attributes and printMe()
and getWeight()
methods inherited
from WeightyThings.Thing
. WeightyThing.Thing
, meanwhile,
contains code and data from both MyAspect
and Weighing
. We've
just customized our original aspect to create our own variant of
the original component.
Notice that we did not need to change the source of the original aspect in any way. Also notice that our second aspect is only a partial definition and does not reference the first aspect in any way. This means that someone (re)using these aspects can choose how they will be combined with other aspects they may wish to add. That is, they can choose which transparencies they will layer together, and in what order.
Aspect Arithmetic and Inheritance
Adding aspects together produces a new aspect. For numbers, it doesn't matter what order you add them in, but for aspects, the order can make a difference, just as with transparencies. For our examples thus far, the order of combination is unimportant, because neither aspect overrode definitions from of the other. This is similar two two transparencies, one of which is blank on its left side, the other blank on the right. If you place these atop each other, it doesn't matter what order you put them in because the non-blank sides show through the blank sides.
But what if one transparency is blank on the left, and the other is blank on the bottom? Then the order will make a difference to the upper right-hand corner of the image, as one transparency or the other will dominate there. What does this look like in terms of classes? Let's see:
class Chatty(Weighing): class Thing: def printMe(self): print "Hi, I'm a",self.__class__.__name__, print "and I weigh", self.getWeight() ChattyThings = (MyAspect + Chatty)(name="ChattyThings")
This new aspect, Chatty
, redefines the printMe()
method to
include the weight of the Thing
it is describing. Because this
requires a getWeight()
method, we derive Chatty
from Weighing
instead of creating it as a new aspect from scratch. In general,
however, you should avoid deriving an aspect from another in this
way, as it reduces reusability. It's sort of like fusing two
transparencies together to make a new one: it's easier to handle
just one, but if you keep them seperate, there's always the option
of placing other transparencies between them. As with class
inheritance, an aspect which inherits from another aspect is
effectively fused to the first one, and you cannot insert new
layers between the two.
Because it redefines printMe()
method, the Chatty
aspect is
also order-sensitive when combined with MyAspect
. If we were to
do this:
ChattyThings = (Chatty + MyAspect)(name="ChattyThings")
We would get a very different result. Adding MyAspect+Chatty
causes the definitions in Chatty
to override any in MyAspect
,
while adding Chatty+MyAspect
causes definitions in MyAspect
to
override those in Chatty
.
Although aspect addition is not commutative (i.e., it's potentially
order-sensitive), it is associative. That is,
(Aspect1+Aspect2)+Aspect3
produces an equivalent result to
Aspect1+(Aspect2+Aspect3)
. This is perfectly in keeping with our
transparency analogy: if the overall order is the same, it doesn't
matter whether you stack transparencies on the projector one at a
time or in previously stacked batches.
Let's look at another way to make 'ChattyThings?':
class Chatty2(Aspect): class Thing: def printMe(self): print "Hi, I'm a",self.__class__.__name__, print "and I weigh", self.getWeight() ChattyThings2 = (MyAspect + Weighing + Chatty2)(name="ChattyThings2")
This time, we didn't derive Chatty2
from Weighing
, so we must
ensure it's there when we create our final component. Notice, by
the way, that because the Weighing
aspect doesn't override
anything in the other two aspects, or vice versa, it doesn't matter
where Weighing
is placed in the list of aspects being added. It
could be first, second, or third. The only ordering constraint here
is that Chatty2
must come after MyAspect
so that it can
override the printMe()
method.
Inheritance Inside Aspects
Sharp-eyed readers will have noticed some time back that the Rock
and Dog
class fragments in our Weighing
aspect do not include
Thing
in their base classes, and that Chatty
and Chatty2
do not
even define the Rock
and Dog
classes! Yet, the WeightyThings
and ChattyThings
components contain Rock
and Dog
classes
which inherit from the respective Thing
classes. How can this be
so?
We already know that TransWarp combines the definitions of the
nested classes. Clearly, this combination includes base classes,
or else Dog
and Rock
wouldn't be inheriting from Thing
. Specifically,
when classes are combined to form a new class, their base class lists are
added together, and duplicates are removed. The overall order of base classes,
however, is preserved, based on the order in which the aspects are combined.
However, this doesn't explain why everything ends up inheriting
from a new class. What's happening is that base classes which are
present in their parent class' dictionary are referenced by name,
not by value. In other words, when generating a component, the
aspect sees that Rock
has a base class Thing
which is defined
in the same outer class, and substitutes the string "Thing"
for
the actual Thing
class in the list of bases. Then, once the
Thing
class has been generated, the Rock
class is generated
with the new Thing
class in the correct place in its __bases__
list.
When a class inherits from another class in the same aspect like this, it is said to have "vertical inheritance" (implying a reference upward in the source code), and the class it inherits from is said to be a "vertical base". In our examples so far, we have dealt only with vertical inheritance and bases.
The other kind of inheritance that a class in an aspect can have,
is when it inherits from a class outside the aspect. This is
called "horizontal inheritance", to imply a reference "sideways" to
another source file. The class inherited from is referred to as a
"horizontal base". TransWarp does not do anything special with
horizontal bases; it simply leaves them as-is in the output class'
base list. This allows you to include special base classes in your
aspects that you don't want redefined. (For example, ZODB's
Persistence.Persistent
mixin.)
Let's recap.
- Aspects can be combined by adding them together, or extended through inheritance.
- Extending through inheritance is less flexible since other aspects cannot be layered between the extended aspect and its base. If seperate aspects were created, then added, the pieces would be individually usable and re-combinable.
- When aspects are combined, so are their nested classes or aspects.
- When classes or aspects are combined, so are their base class lists, and "vertical bases" are replaced with a generated (and combined) class of the same name.
- Combining aspects is associative but not commutative. That is, grouping doesn't make a difference, but order often does.
Using Components
Now that you know how to make aspects and combine them to create custom components, what good is it?
XXX to be continued... XXX
Basic Techniques
- It's not a class until you weave it
- Done all at once so exact implementation data is known, allowing for possibility of optimization features/transforms
- Simple way to weave - just call the aspect
- Optional parameters - name= and module=, recommended for persistence
- name vs.
__name__
side note - nested classes have nested names, but still accessible as self.classname
- name vs.
- Other parameters, for advanced usage
- Optional parameters - name= and module=, recommended for persistence
- Woven aspect is a plain class or extensionclass, with nested classes all the way down. Can now be used to make instances like a regular class.
- Style note: name aspects using gerunds (e.g. "Parsing") or descriptive words/phrases (e.g "Colorable", "Persistent") rather than nouns
Combining Aspects
- Using Addition
- Using Inheritance
- Precedence/ordering of feature overrides
- Inheritance Among Nested Classes
- "Horizontal" vs. "Vertical" Inheritance
- Nested classes may inherit from each other - the relationship will be preserved in the final class hierarchy by name. (need example)
- Inheritance from classes not contained in the aspect will be preserved by value.
- Base class list in output classes is left-to-right "first come, first served" from source aspects.
- "Horizontal" vs. "Vertical" Inheritance
Intermediate Techniques
- How Features Are Combined
- Using FeatureDef's
- Using Transforms
- Handling Persistence
Advanced Techniques
- Creating new FeatureDef classes
- Creating new Transform classes
- Programmatically Creating Aspects
- Places to look if you want to create far-out meta-stuff