Introducing the concept of Extendable Object Faces (API)

Submitted by fago on Mon, 07/06/2009 - 20:29
Preliminary Warning: "If you're afraid of classes and objects in PHP, run away now." - jpetso. Update: For simplicity the API has been changed so that all faces are incorporated, thus modules have to care about naming collisions theirself. Also in the meantime file inclusions support has been added. While figuring out a object oriented design for the rules engine I recognized the need for a possibility to allow modules extend objects in various places. Thus I developed a generic concept which does just that: Allowing modules to extend objects. I called the concept "Extendable Object Faces", which basically implements the Facade pattern in a modular way. So let's have a closer look at that. A module that wants to extend an object may only do so on top of a defined interface, preventing uncontrolled growing objects. Thus one can write some code, define an interface for it and attach it to potential any extendable object out there. Then other modules have an easy way to test whether some object has a functionality in place by checking for the availability of a certain interface. To use that functionality the caller has to use the right "Face" of the object - corresponding to a certain interface. This approach using different "Object Faces" make sure there can't be conflicting method names, so modules don't need to prefix their methods with the module's name which would result in ugly and not readable code. I've already implemented an initial version of the Extendable Object Faces API. As of now you can:
  • Extend an object by providing an Extender Class
  • Extend an object simply by some functions, each implementing a method
  • Override any dynamically added method by providing functions or an Extender Class
So apart from "allowing modules to extend an object", this also allows one to:
  • Easily lazy load huge parts of an objects implementation, invisible for the caller.
  • Allow modules to dynamically alter an implementation by using overriding.
Hence such an Extended Object can also serve as a clean abstraction for module provided callbacks! Enough talk, let's show how it works:
<?php
 
interface FacesTestInterface {
  function
isWorking($prefix);
}

/**
 * Extendable Class
 */
class FacesTestElement extends FacesExtendable {
 
//Your code here..
}
?
?>
This code provides the class for the Extendable Object and already defines an interface, which modules may implement. So let's extend it with an Extender Class.
<?php
 
/**
 * Extender Class
 */
class FacesTestExtender extends FacesExtender implements FacesTestInterface {

  function
isWorking($prefix) {
    return
$prefix . $this-
?>
object->name; } } ?> That's it. Now you can use the face!
<?php
 $element
= new FacesTestElement();
   
$element-
?>
name = 'test'; $element->extendByClass(array('FacesTestInterface'), 'FacesTestExtender'); // Now use it. print $element->face('FacesTestInterface')->isWorking('Name:'); // This prints "Name:test". ?> As you see, one has to use the face to be able to use the added method - so potential name collisions are avoided. However the Extendable Class can define so called incorporated faces, which are built in the Extendable Object as soon as a module provides an implementation:
<?php
 
/**
 * New extendable Class
 */
class FacesTestElement extends FacesExtendable {
  public function
getIncorporatedFaces() {
    return array(
'FacesTestInterface');
  }
}
?
?>
So you can use it that way, once extended:
<?php
 
print $element-
?>
isWorking('Name:'); ?> You can find more examples in the simpletests I've written. Also checkout the benchmarks I've run. So what's missing? Basically the implementation is complete and working fine. Though feedback and suggestions are very welcome! However currently the code doesn't deal with including code where the implementation can reside (lazy loading), though for drupal 7 that is already solved by the code registry (just add one call to drupal_function_exists()). For drupal 6 I think about adding the possibility to specify include files per added interface.

Dreaming...

If we would have an object oriented "Data API" in drupal core, this could serve as a way to let modules extend those objects. So instead of writing node-centric code code could would be written in a generic way - attachable to potentially each of those data objects (users, comments, terms). Having such generic code would allow us to finally end the "Everything should be a node" debate. Apart from that this would help us to easily lazy load big chunks of code, just exploiting the code registry!

Wim Leers@drupal.org (not verified)

Mon, 07/06/2009 - 23:39

Did you completely come up with this yourself? It's fairly easy to grasp, but I think it'll only start making sense completely — and that it will only let you feel its power — when you're actually using it. Could you maybe explain in more detail how exactly this solves a particular need/problem you had with Rules? And what painful process you'd have to apply/go through if you didn't write this first? I think that would help convince others :) I can see the use myself, but would still appreciate a fairly detailed example. Worst case: very interesting concept. Best case: awesome enabling technique for a better developer experience. In both cases: thanks :)

>Did you completely come up with this yourself? Yep, however I studied crell's posts about the topic as well his implementation of extenders for the db API, which is working a bit different, it completely wraps the extended object. >Could you maybe explain in more detail how exactly this solves a particular need/problem you had with Rules? I'd like to, though I've not really used it myself yet (except for the unit tests). I'm going to base the object-oriented rework of rules on it, but it's not done yet. The initial need there was for the rules data API, which provides an unique interface for all data types. In regard to properly support different web service types I need some format conversion, like to XML or to RDF/XML for those data types. The extendable object faces make it possible that other modules add those format conversion for other data types in a clean way, consider the RDF module adding the possibility to transform nodes to RDF. Another point were I needed a solution were the rule objects itself, where I wanted to load UI related code only on respective admin sites, so rule evaluation stays slim and fast. However I'd prefer easy method call to access it, so that anyone can easily reuse the admin forms e.g. just by calling and embedding $rules->form(). An incorporated object face fits perfectly for that. Then I realized that this API would be useful in other places too, e.g. I could completely embed the include mechanism into it - simplifying the rest of the code. Also a default extender together with dynamic overridden methods fits perfectly for the implementation of actions/conditions, which have different callbacks per action/condition - thus it eliminates the need to explicitly deal with the callbacks in the code. However I still have to figure out a way to properly setup those rules objects, so that getting them from cache works fast. Of course one could just call drupal_alter() on the objects, however that's too slow for my case. So probably I'm going to build a setup mechanism into the rules objects, which gets the extension information from cache - of course modules have to provide that information in a hook like hook_rules_plugin_registry() or so. >And what painful process you'd have to apply/go through if you didn't write this first. I can't really imagine that ;) Probably there what be a kind of factory to get UI-related object for a usual rules object, making it more complicated to use. For the data API and its format, probably each format would create its own API and modules using it would have to specifically rely on that. Thus, I think the Faces really help making APIs easier to use and understand, thus you don't have to care which module implements the functionality or what needs to be included, just use it. So you just have to know the facade, forget the internals.