Tuesday, January 24. 2006
Making Fluent Interfaces Readable in PHP
There is a lot of talk about fluent interfaces out there in the PHP blogsphere. I like the idea of fluent interfaces, they're great mojo, but like most mojo, it takes some care.
What are Fluent Interfaces anyway?
Fluent Interfaces are a method of programming where setters, and sometimes behavioural methods that normally return void/null actually return an object instance, usually (but not always) the object being operated on. This is so that you can get around the effect where you make a temporary variable to alter an object, but don't end up using it again.
Fluent Interfaces were first developed discovered by Eric Evans, and described by Martin Fowler in his bliki. Mike Naberezny Describes fluents quite succinctly, especially if you are a PHP guy.
However, there seem to be some complaints about the readability and usability of Fluent interfaces, (especially if you check Jason E. Sweat's blog). One relevant criticism (brought up by Jason in his comment section) is that it violates the Principle of Least Surprise.
Making Fluent Interfaces... Fluent
The key to making fluent interfaces readable, workable and—dare I say—fluent, is to sprinkle in some magic. If you define an empty interface called "Fluent", and any time you build a class that is to be fluent, then you are in effect signalling to other users of the class that you are providing a fluent interface. Easy hey?
interface Fluent
{
}
But check this out, the real magic is here. Using PHP's magic __call() method, you can build a wrapper class to make a non-fluent class in to a fluent class quite easily.
class Fluenter
{
private $obj;
function __construct($obj)
{
$this->obj = $obj;
}
static function MakeFluent($obj)
{
if ($obj instanceof fluent)
return $obj;
else
return new fluenter($obj);
}
function __call($method, $args)
{
$result = call_user_func_array(array($this->obj, $method), $args);
if (is_null($result))
return $this;
else if (is_object($result) and ($result instanceof $fluent))
return $result;
else
throw new RuntimeException(
"Fluent::__call called method $method ".
"and expected a null return or a non-fluent object, ".
"got (".gettype($return).") $return instead.");
}
}
Using the Fluenter
class NonFluentClass
{
private $a, $b, $c, $d = array();
function __construct($a, $b)
{
$this->a = $a;
$this->b = $b;
}
function setC($c)
{
$this->c = $c;
}
function addD($d)
{
$this->d[] = $d;
}
}
class FluentClass implements Fluent
{
function __construct($a, $b)
{
$this->a = $a;
$this->b = $b;
}
function setC($c)
{
$this->c = $c;
return $this;
}
function addD($d)
{
$this->d[] = $d;
return $this;
}
}
$foo = new nonFluentClass(2,3);
Fluenter::MakeFluent($foo) ->setC(5)
->addD(6)
->addD(23);
echo "NonFluentClass turned into a Fluent with Fluenter::MakeFluent:\n";
var_dump($foo);
echo "\n\n";
echo "FluentClass used with Fluenter::MakeFluent:\n";
$bar = new FluentClass(2,3);
Fluenter::MakeFluent($bar) ->setC(5)
->addD(6)
->addD(23);
var_dump($bar);
Output
NonFluentClass turned into a Fluent with Fluenter::MakeFluent:
object(NonFluentClass)#1 (4) {
["a:private"]=>
int(2)
["b:private"]=>
int(3)
["c:private"]=>
int(5)
["d:private"]=>
array(2) {
[0]=>
int(6)
[1]=>
int(23)
}
}
FluentClass used with Fluenter::MakeFluent:
object(FluentClass)#2 (4) {
["a"]=>
int(2)
["b"]=>
int(3)
["c"]=>
int(5)
["d"]=>
array(2) {
[0]=>
int(6)
[1]=>
int(23)
}
}
Notice how the Fluenter class handles fluent and non-fluent objects relatively gracefully. Also by using the syntax Fluenter::MakeFluent($obj) we are explicitly telling the programmer that the next chunk of code is being programmed in a fluent style. With some proper class documentation, programmers unaware of the fluent style could easily become educated.





It is often said that when you travel the world, you will learn more about your home country then you will of the host country. I feel that the same is true of programming languages. Learning a new language is like visiting a country for awhile, and som
Tracked: Mar 06, 12:16