WEB Advent 2010 / Aspect-Oriented Design

I try to write simple apps. I try to code for today, as Lorna articulated so well. Almost invariably, I end up with something more complex than I intended. One thing that has helped immensely is unit testing. Not only does it help me reduce complexity, it provides flexibility, which is almost as important.

Flexibility lets me adapt to the changing requirements that are often the cause of more complex code. I need a particular feature by the end of the day on Sunday, so I’ll just write a class that extends the current functionality. Maybe I actually have to replace more functionality than extending and overriding will allow, so I will add in a conditional to load a totally new class. Whatever the case, changes are happening so fast that architecting a smarter approach makes little sense on Saturday night.

I tried to solve this problem once before by adding in methods of the parent class that would get called, but nothing would actually happen unless the child implemented them. The result was a callback system that allowed me to add in functionality, but it never allowed the underlying method to change.

Luckily, I have this buddy Nate who found himself in a similar predicament. He wanted more flexibility, he wanted simpler code, and he was not happy with the callback approach. At about the same time, PHP introduced some new functionality in the form of anonymous functions. With new tools in hand, a knowledge of aspect-oriented programming, and a deep desire for more flexibility, a new approach called method filters developed. This is one of the software architectures that powers our new framework, Lithium.

Aspect-oriented programming desires to solve the problem of injecting functionality into concrete methods of a class. This approach allows us to extend or replace individual methods without having to actually extend the class itself. The intention is to take disparate programming concerns, or aspects, and disentangle them from the business logic of our code. While the concept of AOP itself provides a great deal of flexibility, traditional implementations involve whole new layers, complete with new and unintuitive terminology, and code preprocessing. In other words, not too simple.

The filters approach, on the other hand, gives us both flexibility and simplicity. Below is a simple example of a possible Filters class. This class mimics the behavior of the one found in Lithium, but with much less elegance and robustness.

class Filters {

	protected static $_filters = array();

	public static function add($method, $callback) {
		static::$_filters[$method][] = $callback;
	}

	public static function run($method, $object, $params, $filter) {
		static::add($method, $filter);
		return static::next($method, $object, $params);
	}

	public static function next($method, $object, $params) {
		$next = array_shift(static::$_filters[$method]);
		return $next($method, $object, $params);
	}
}

The add method allows us to stack anonymous functions as filters. The run method is used by class methods that should be filterable. The next method returns the result of the next filter in the stack. The run and next methods both accept the instance of the class to be filtered and the parameters that were passed to the class’s original method. These parameters are then passed into the anonymous function, so we have access to everything in the original method.

To take advantage of this Filters class, we simply have to wire our methods to accept these parameters. Below, I create a basic class that might interact with the database. It has a read method that we want to be filterable. To do this, we pass the method signature, Database::read, represented by the __METHOD__ constant, then pass the current instance, the parameters of the original method, and finally a closure for the basic functionality.

class Database {

	public function read($query, array $options = array()) {
		$params = compact('query', 'options');

		return Filters::run(___METHOD__, $this, $params, function ($method, $self, $params) {
			$result = 'the basic method functionality';
			return $result;
		});
	}
}

Now that the read method is filterable, we might want to add some logging. Below is an example filter that attaches the anonymous function to the Database::read method.

Filters::add('Database::read', function ($method, $self, $params)) {
	file_put_contents('/tmp/log', var_export($params, true), FILE_APPEND);
	return Filters::next($method, $self, $params);
});

These two pieces of code provide interesting insight into how filters work. Filters are a collection of anonymous functions that happen in a first-in, first-out order. We stack multiple filters, and a filter can do anything with the chain that comes after.

Here is another simple example that allows us to log the result of the read method.

Filters::add('Database::read', function ($method, $self, $params)) {
	$result = Filters::next($method, $self, $params);
	file_put_contents('/tmp/log', var_export($result, true), FILE_APPEND);
	return $result;
});

Using this technique to separate logging code from the rest of our system also means that rather than being scattered throughout our app, all of our logging code can be organized in one authoritative place. The same can be done with other such concerns including caching, access control, dealing with translations, &c.

Filters provide a large amount of flexibility, and, when used responsibly, can result in much simpler code. We can do anything before or after, or even completely intercept the method in the chain. Of course, the flexibility this approach provides can also lead to increased complexity if we are not responsible. We have to constantly be aware where we are modifying functionality, but side effects are inherent in any system. Still, building in flexibility with a system like filters ultimately keeps the code simpler, longer.

Since I brought up Lithium’s filter system before, you might be interested in seeing how it compares. The Filters class is part of the util package. Together with the core package, Filters could easily be added to any project. In the example above, the Filters class holds everything that is expected to happen on particular method. In Lithium, each class maintains the stack of filters to be used. Lithium accomplishes this by having any class with filter methods extend Object or StaticObject. Another difference is the parameters that need to be passed and calling static methods to achieve the functionality. Lithium implements Filters as an Iterator, so traversing the stack is much cleaner.

As a parting thought, you might be wondering how I use filters given the scenario I mentioned the beginning. Supposing that we need a particular feature by Sunday, and enabling this feature involves modifying the method of a pre-existing class in the system, I could quickly make the method filterable as was done in the first code example, then add my desired functionality to a shiny new filter. If, at a later point, I recognize that this feature needs to be more fundamental to the system, I can easily refactor the filter into a more concrete implementation.

Other posts