Wrappable methods are special methods on an Object class whose invocations be dynamically 'wrapped' with new behavior before and/or after the invocation of the wrapped method. For Python coders method wrappers are similar to method decorators, for Aspect-Oriented folks this is a poor man's join-point, for everyone else wrappable methods are a flexible extensibility point where you can flexibly inject new behavior.
Consider inheritance in traditional object-oriented programming. By overriding a method in a sub-class you can effectively wrap behavior around the parent class' method call. Here is some example code:
| |
| |
Now, we call the 'wrapped', or in this case overridden, parent
class' | |
Finally, let's print an after message once the call to
|
In this simple example we've effectively wrapped
MyBaseClass' foo method
with some new behavior: we print 'Before!' and 'After!'
foo is called on instances of
MySubClass. What is so bad about pure
inheritance?
In many cases: Nothing, when you can use it, use it! If you can override methods to implement the functionality you need it is the best, most efficient way of 'wrapping' behavior around a method. Sometimes, though, it's painfully inflexible.
Inflexibility - Inheritance is heirarchical. Our goal in
MySubClass above was to print messages before
and after a method call. Let's say we also wanted to log the outcome
of the call to foo. We could create a new
LoggedSubClass class that subclasses
MySubClass and logs the result. Now we're
ready to deploy and we want to get rid of the wrapped echo'ing
behavior, to do so we must either change the parent class of
LoggedSubClass or comment/toggle out the
behavior in MySubClass.
Duplication of Code - Suppose we wanted to have printing and logging capabilities wrap a bunch of method calls on a bunch of other classes. Without reorganizing the entire inheritance hierarchy we must duplicate similar code in many places around the application.
Other languages and programming paradigms have a fairly simple
solution to these limitations, as mentioned Python's decorators and AOP's
join-points, that Recess looked towards for inspiration. Recess' solution
is called 'Wrappable' methods. A !Wrappable method can
be wrapped by, (or decorated with), unbounded
IWrappers that can be composed dynamically at
runtime. Let's take a look at how we could make
foo a wrapped method:
The | |
The actual name of the method is wrappedFoo. The only
restriction on naming the wrapped method is that it can not have the
same name as the wrappable method. By convention Recess prefixes
wrapped methods with | |
We can now invoke a method |
Great! We have a wrappable foo method, now let's wrap it
with printing behavior. All we need to do is create a
PrintWrapper class that implements the
IWrapper interface.
Example 6.6. Implementing IWrapper
// ... replace starting at previous example's definition of PrintWrapper ... class PrintWrapper implements IWrapper { function before($object, &$arguments) {echo "Before!\n"; } function after($object, $returns) {
echo "After!\n"; return $returns; } function combine(IWrapper $wrapper) { return false; }
} MyBaseClass::wrapMethod('MyBaseClass', 'foo', new PrintWrapper());
$obj = new MyBaseClass(); $obj->foo();
$obj->wrappedFoo();
// Output: // Before! // Foo! // After! // Foo!
PrintWrapper implements the three methods
defined in the IWrapper interface. Let's discuss
each:
| |
| |
| |
Here we use the static method
| |
By invoking foo we first work our way through
| |
For the sake of being thorough we show that you can still call
|
We now have the same printing behavior as in our
Inheritanc example, but without using inheritance. The power and
flexibility of wrappable methods and wrappers is that multiple wrappers
can wrap a wrappable method. So we could create a logging wrapper that
also wraps foo and easily flip either wrapper on
or off, dynamically at run-time, without having to reorganize our class
hierarchy. For more detail see the section called “The
Mechanics of Wrapped Methods and Wrappers”. Lets point out
exactly how wrappable methods address the downsides of inheritance:
Wrappable methods avoid the inflexibility of inheritance-based overriding because methods can be wrapped dynamically. Wrapping methods with new functionality does not affect the type system or class hierarchy.
Wrappable methods encapsulate a behavior around a method call. This presents a new way for PHP programs to package functionality to fight code duplication. To use Aspect-Oriented Programming terminology you could think of wrappers as a means for separating crosscutting concerns. Cross-cutting concerns are tasks like logging/printing debug messages as shown in the example above, authorization and access control, etc.
Given these two specific characteristics wrappable methods and
wrappers provide a natural extensibility model. Plugin-developers can
implement IWrappers that application-developers can
easily incorporate in their projects because there is no need to modify a
class heirarchy and the plugin's wrapper behavior is encapsulated in a
simple class. For application-developers, instantiating wrappers and
applying them with the
Object::wrapMethod API can
be awkward and cumbersome. This is where Recess Core's annotations come to
the rescue, annotations provide the perfect vehicle for making declarative
statements about a class or method which then employ wrappers and attached
methods to do the leg work under the covers. For more information on
annotations, see the section called “Annotations”
Where in the code base are wrapped methods implemented? What is the exact logic for processing methods wrapped with multiple wrappers? The answers to these questions are the focus of this section.
The implementation of wrapped methods can be thought of as a
combination of the Observer and Strategy design patterns with specific
semantics. Wrappers are observers of wrapped methods who are notified
before and after the wrapped method is called. The
before and after
aren't vanilla notifications, though, and can return values that affect
the logic of the call similar to a strategy. The logic of wrapped method
invocation is implemented in the
recess.lang.WrappedMethod class. The
addWrappedMethod in
recess.lang.ClassDescriptor brings wrapped
methods onto a class' descriptor, and, finally the
recess.lang.WrappableAnnotation abstracts away
the pattern of making a plain-old class method a wrappable
method.
When a wrapped method is invoked, the following process occurs:
Statement S invokes wrapped method M on object O with arguments A*.
Each wrapper's before method is
invoked in the reverse order that the wrappers were added[2](LIFO). before is passed, by
reference, O and an array of
A*. The wrapper, thus, has an opportunity to
get or set public state from O or any
argument in A*. If a wrapper's
before does not return a value or returns
the value null then the next wrapper's before
is called until all wrapper's before
methods have been called. If a wrapper's
before returns a non-null value this
value does not pass go and does not collect two hundred dollars,
it short-circuits the wrapper call-chain and is immediately
returned to statement S.
The call to the wrapped method M is made using the (potentially transformed) arguments A*. M returns value R.
Each wrapper's after method is
invoked in the order that the wrappers were added (FIFO).
after is passed arguments
O and R
(M's return value). If the call to a
wrapper's after returns a non-null value
then this return value, R', will override
R in the remaining wrapper's calls to
after, else R' is
set to R.
The value R' is returned to S.
While nothing will stop you as an
IWrapper author from writing the following at
design-time, it should be noted that these practices will most
likely cause errors and headaches at run-time and are considered
really bad practice:
In before: changing the types
of arguments in A*, or changing the
number of arguments in A*.
A* must remain such that using its
elements to call method M will result in
a valid method call with the arguments M
expects.
In before: returning a value of
any type other than M could be expected
to return.
In after: returning a value
R' of any type other than
M could be expected to return.
At runtime each wrapper is an instantiated object. In production
mode these objects are deserialized on every request. Reducing the
number-of wrappers is a boost to performance in time (extra method calls
are expensive) and space so Recess gives IWrapper
authors a simple way to combine similar wrappers. Imagine you've just
created a !Required annotation that application
developers can place on properties of a Model to
denote they are required for insert and
update. Beyind the scenes you've written a
RequiredWrapper that takes the name of a property
and in the before method checks to make sure
the property's value is non-null. Each annotation would thus expand to
wrap insert and update
with a new instance of RequiredWrapper for every
property on the model. That could mean a lot of
IWrapper objects to call
before and after on to
check requiredness! (It would also mean you couldn't check more than one
field for requiredness because of short-circuit returns!)
When wrappers are applied to a
WrappedMethod using
addWrapper the
WrappedMethod first iterates through each of the
existing wrappers and calls their combine
method, passing in the new wrapper. If the existing wrapper determines
it can combine its state with the new wrapper's state it will do so and
return true which indicates to the WrappedMethod "do not add
this new wrapper to your list, I've taken on its duties". If
all existing wrapper's combine method returns
false the new wrapper will be added to the list of registered wrappers.
Let's take a look at an example:
Example 6.7. Combining Wrappers
<?php class RequiredWrapper implements IWrapper { protected $properties = array();function __construct($property) { $this->properties[] = $property; } function before($model, $args) { $missing = array(); foreach($this->properties as $property) { if($model->$property === null) { $missing[] = $property; } } if(!empty($missing)) { print("The following properties are required: " . implode(", ", $missing); return false;
} else { return null;
} } function after($model, $returns) { return $returns; } function combine(IWrapper $that) { if($wrapper instanceof RequiredWrapper) { $this->properties = array_merge($this->properties, $that->properties);
return true;
} else { return false;
} } } MyModel::wrapMethod('MyModel', 'insert', new RequiredWrapper('fieldA')); MyModel::wrapMethod('MyModel', 'insert', new RequiredWrapper('fieldB')); MyModel::wrapMethod('MyModel', 'update', new RequiredWrapper('fieldB')); ?>
We'll store the list of required property names in | |
Here we short-circuit return | |
Returning null is not necessary, it is the same as not returning, shown to illustrate that if all required fields are non-null the call will pass through to the wrapped call just fine. | |
Here we combine the state of two
| |
If we can combine we return | |
If we cannot combine we return
|
The result of this code is that there will be two
RequiredWrapper instances, one for
insert and the other for
update. The
RequiredWrapper for insert will contain two
properties in its $properties array. It is important
to note that new instances of wrappers should be wrapped for each
method, for example, if the same instance of a new
RequiredWrapper('fieldB') had been wrapped around insert and
update then fieldA would be required for update as well because of
combine. (Note: This doesn't pass the sniff test and exposes
too much guts. Maybe we could change the implementation of
addWrapper in
WrappedMethod to clone the Wrapper before adding
it.)
[2] IWrapper implementations must not
depend on the order in which they are applied to a wrappable
method.