Aspect
Aspect - Aspect-oriented programming (AOP) for Perl
package Person;
sub create { ... }
sub set_name { ... }
sub get_address { ... }
package main;
use Aspect;
# using reusable aspects
aspect Singleton => 'Person::create'; # let there be only one Person
aspect Profiled => call qr/^Person::set_/; # profile calls to setters
# append extra argument when Person::get_address is called:
# the instance of the calling Company object, iff get_address
# is in the call flow of Company::get_employee_addresses.
# aspect will live as long as $wormhole reference is in scope
$aspect = aspect Wormhole => 'Company::make_report', 'Person::get_address';
# writing your own advice
$pointcut = call qr/^Person::[gs]et_/; # defines a collection of events
# advice will live as long as $before is in scope
$before = before { print "g/set will soon be called" } $pointcut;
# advice will live forever, because it is created in void context
after { print "g/set has just been called" } $pointcut;
before
{ print "get will soon be called, if in call flow of Tester::run_tests" }
call qr/^Person::get_/ & cflow tester => 'Tester::run_tests';
Aspect-oriented Programming (AOP) is a programming method developed by Xerox PARC and others. The basic idea is that in complex class systems there are certain aspects or behaviors that cannot normally be expressed in a coherent, concise and precise way. One example of such aspects are design patterns, which combine various kinds of classes to produce a common type of behavior. Another is logging. See http://www.aosd.net for more info.
The Perl Aspect module closely follows the terminology of the AspectJ
project (http://eclipse.org/aspectj). However due to the dynamic nature of
the Perl language, several AspectJ features are useless for us: exception
softening, mixin support, out-of-class method declarations, and others.
The Perl Aspect module is focused on subroutine matching and wrapping. It
allows you to select collections of subroutines using a flexible pointcut
language, and modify their behavior in any way you want.
Person, that are in the call flow of some Company, but
not in the call flow of Company::make_report. Aspect supports
call(), and cflow() pointcuts, and logical operators (&, |, !)
for constructing more complex pointcuts. See the Aspect::Pointcut
documentation.
CODE ref. Match currently running sub, or a sub in the call
flow. Build pointcuts composed of a logical expression of other pointcuts,
using conjunction, disjunction, and negation.
CODE ref for matched
sub, and access the context of any call flow pointcuts that were matched, if
they exist.
Perl is a highly dynamic language, where everything this module does can be done without too much difficulty. All this module does, is make it even easier, and bring these features under one consistent interface. I have found it useful in my work in several places:
Test::Class
test method, because I use the TestClass aspect.
caller() and Hook::LexWrap, but
this is much easier.
The Aspect module is different from Hook::Lexwrap (which it uses for the
actual wrapping) in two respects:
Account objects that are in the call flow
of Company::make_report.
This package is a facade on top of the Perl AOP framework. It allows you to
create pointcuts, advice, and aspects. You will be mostly working with this
package (Aspect), and the advice context package.
When you use this package:
use Aspect;
You will import five subs: call(), cflow(), before(), after(), and
aspect(). These are all factories that allow you to create pointcuts,
advice, and aspects.
Pointcuts select join points, so that an advice can run code when they happen.
The simplest pointcut is call(). For example:
$p = call 'Person::get_address';
Selects the calling of Person::get_address(), as defined in the symbol
table during weave-time. The string is a pointcut spec, and can be expressed
in three ways:
string
regexp
Select only the subs whose name matches the regexp. The following will match
all the subs defined on the Person class, but not on the Person::Address
class.
$p = call qr/^Person::\w+$/;
CODE ref
Select only subs, where the supplied code, when run with the sub name as only
parameter, returns true. The following will match all calls to subs whose name
isa key in the hash %subs_to_match:
$p = call sub { exists $subs_to_match{shift()} }
Pointcuts can be combined to form logical expressions, because they overload
&, |, and !, with factories that create composite pointcut objects.
Be careful not to use the non-overloadable &&, and || operators, because
you will get no error message.
Select all calls to Person, which are not calls to the constructor:
$p = call qr/^Person::\w+$/ & !call 'Person::create';
The second pointcut you can use, is cflow(). It selects only the subs that
are in call flow of its spec. Here we select all calls to Person, only if
they are in the call flow of some method in Company:
$p = call qr/^Person::\w+$/ & cflow company => qr/^Company::\w+$/;
The cflow() pointcut takes two parameters: a context key, and a pointcut
spec. The context key is used in advice code to access the context (params,
sub name, etc.) of the sub found in the call flow. In the example above, the
key can be used to access the name of the specific sub on Company that was
found in the call flow of the Person method.The second parameter is a
pointcut spec, that should match the sub required from the call flow.
See the Aspect::Pointcut docs for more info.
An advice is just some definition of code that will run on a match of some
pointcut. An advice can run before the pointcut matched sub is run, or after.
You create advice using before(), and after(). These take a CODE ref,
and a pointcut, and install the code on the subs that match the pointcut. For
example:
after { print "Person::get_address has returned!\n" }
call 'Person::get_address';
The advice code is run with one parameter: the advice context. You use it to
learn how the matched sub was run, modify parameters, return value, and if it
is run at all. You also use the advice context to access any context objects
that were created by any matching cflow() pointcuts. This will print the
name of the Company that started the call flow which eventually reached
Person::get_address():
before { print shift->company->name }
call 'Person::get_address' & cflow company => qr/^Company::w+$/;
See the Aspect::AdviceContext docs for some more examples of advice code.
Advice code is applied to matching pointcuts (i.e. the advice is enabled) as long as the advice object is in scope. This allows you to neatly control enabling and disabling of advice:
{
my $advice = before { print "called!\n" } $pointcut;
# do something while the device is enabled
}
# the advice is now disabled
If the advice is created in void context, it remains enabled until the interpreter dies, or the symbol table reloaded.
Aspects are just plain old Perl objects, that install advice, and do other AOP-like things, like install methods on other classes, or mess around with the inheritance hierarchy of other classes. A good base class for them is Aspect::Modular, but you can use any Perl object.
If the aspect class exists in the package Aspect::Library, then it can be
easily created:
aspect Singleton => 'Company::create';
Will create an Aspect::Library::Singleton object. This reusable aspect is
included in the Aspect distribution, and forces singleton behavior on some
constructor, in this case, Company::create().
Such aspects, like advice, are enabled as long as they are in scope.
Due to the dynamic nature of Perl, and thanks to Hook::LexWrap, there is no
need for processing of source or byte code, as required in the Java and .NET
worlds.
The implementation is very simple: when you create advice, its pointcut is
matched using match_define(). Every sub defined in the symbol table is
matched against the pointcut. Those that match, will get a special wrapper
installed, using Hook::LexWrap. The wrapper only runs if during run-time,
the match_run() of the pointcut returns true.
The wrapper code creates an advice context, and gives it to the advice code.
The call() pointcut is static, so match_run() always returns true,
and match_define() returns true if the sub name matches the pointcut
spec.
The cflow() pointcut is dynamic, so match_define() always returns
true, but match_run() return true only if some frame in the call flow
matches the pointcut spec.
Support for inheritance is lacking. Consider the following two classes:
package Automobile;
...
sub compute_mileage { ... }
package Van;
use base 'Automobile';
And the following two advice:
before { print "Automobile!\n" } call 'Automobile::compute_mileage';
before { print "Van!\n" } call 'Van::compute_mileage';
Some join points one would expect to be matched by the call pointcuts above, do not:
$automobile = Automobile->new; $van = Van->new; $automobile->compute_mileage; # Automobile! $van->compute_mileage; # Automobile!, should also print Van!
Van! will never be printed. This happens because Aspect installs
advice code on symbol table entries. Van::compute_mileage does not
have one, so nothing happens. Until this is solved, you have to do the
thinking about inheritance yourself.
You may find it very easy to shoot yourself in the foot with this module. Consider this advice:
# do not do this!
before { print shift->sub_name }
cflow company => 'MyApp::Company::make_report';
The advice code will be installed on every sub loaded. The advice code
will only run when in the specified call flow, which is the correct
behavior, but it will be installed on every sub in the system. This
can be slow. It happens because the cflow() pointcut matches all
subs during weave-time. It matches the correct sub during run-time. The
solution is to narrow the pointcut:
# much better
before { print shift->sub_name }
call qr/^MyApp::/ & cflow company => 'MyApp::Company::make_report';
There are a number of things that could be added, if people have an interest in contributing to the project.
* cookbook
* tutorial
* example of refactoring a useful CPAN module using aspects
* new pointcuts: execution, cflowbelow, within, advice, calledby. Sure you can implement them today with Perl treachery, but it is too much work.
* need a way to match subs with an attribute, attributes::get() will not work for some reason
* isa() support for method pointcuts as Gaal Yahas suggested: match methods on class hierarchies without callbacks
* Perl join points: phasic- BEGIN/INIT/CHECK/END
* The previous items indicate a need for a real join point specification language
* look into byte code manipulation with B:: modules- could be faster, no need to mess with caller, and could add many more pointcut types. All we need to do for sub pointcuts is add 2 gotos to selected subs.
* use Sub::Uplevel instead of Hook::LexWrap caller trick, thus we will will play nice with other modules that use Sub::Uplevel (e.g. all the test modules seem to use it)
* a debug flag to print out subs that were matched on match_define
* warnings when over 1000 methods wrapped
* support more pulling (vs. pushing) of aspects into packages: attributes, package specific join points
* add whatever constructs required for mocking packages, objects, builtins
* debugger support: break on pointcut
* allow finer control of advice execution order
* need better example for wormhole- something less tedius
* bring back Marcel's tracing aspect and example, class invariants example
* use Scalar-Footnote for adding aspect state to objects, e.g. in Listenable. Problem is it is still in developer release state
* Listenable: when listeners go out of scope, they should be removed from listenables, so you don't have to remember to remove them manually
* Listenable: should overload some operator on listenables so that it is easier to add/remove listeners, e.g.: $button += (click => sub { print 'click!' });
* design aspects: DBC, threading, more GOF patterns
* middleware aspects: security, load balancing, timeout/retry, distribution
* Perl aspects: add use strict/warning/Carp to all matched packages. Actually, Spiffy, Toolkit, and Toolset do this already very nicely.
* interface with existing Perl modules for logging, tracing, param checking, generally all things that are AOPish on CPAN. One should be able to use it all through one consistent interface. If I have a good set of pointcuts, I should be able to do all kinds of cross- cutting things with them.
* UnderscoreContext aspect: subs that match will, if called with no parameters, get $_, and if in void context, return value will set $_. Allows you to use your subs like builtins, that fall back on $_. So if we have a sub:
sub replace_foo { my $in = shift; $in =~ s/foo/bar; $in }
Then both calls would be equivalent:
$_ = replace_foo($_);
replace_foo;
* a generic FriendParamAppender aspect, that adds to a param list for affected methods, any object the method requires. Heuristics are applied to find the friend: maybe it is available in the call flow? Perhaps someone in the call flow has an accessor that can get it? Maybe a lexical in some sub in the call flow has it? The point is to cover all cases where we pass objects around, so that we don't have to. A generalization of the wormhole aspect.
Please report any bugs or feature requests through the web interface at http://rt.cpan.org/Public/Dist/Display.html?Name=Aspect.
See perlmodinstall for information and options on installing Perl modules.
The latest version of this module is available from the Comprehensive Perl Archive Network (CPAN). Visit <http://www.perl.com/CPAN/> to find a CPAN site near you. Or see http://search.cpan.org/perldoc?Aspsect.pm.
Marcel Grünauer <marcel@cpan.org>
Ran Eilam <eilara@cpan.org>
Adam Kennedy <adamk@cpan.org>
You can find AOP examples in the examples/ directory of the
distribution.
Copyright 2001 by Marcel Grünauer
Some parts copyright 2009 Adam Kennedy.
This library is free software; you can redistribute it and/or modify it under the same terms as Perl itself.