Scriptorama.nl

Header image showing a keyboard, mouse, laptop and books on design patterns

Proxies, Delegates en Decorators met PHP5

Sinds PHP5 is er de mogelijkheid om aanroepen van methodes en aanroepen naar members af te vangen met eigen code. Deze methodes (__get, __set en __call) bieden je de mogelijkheid om zeer generieke proxies, decorators en delegators te implementeren.

Proxies, decorators en delegators hebben 1 ding in gemeen: ze wijzigen alle drie het gedrag van het onderliggende object door een van de methoden te overriden terwijl de rest van de methoden in principe hetzelfde blijft.

Het is niet de bedoeling dat deze post alleen maar theoretisch is, dus laten we een simpel voorbeeld gebruiken om het concept te demonstreren.

Stel dat we de volgende klasse hebben:

PHP:
  1. class HelloWorld
  2. {
  3.   function sayHello()
  4.   {
  5.     return "Hello World";
  6.   }
  7.  
  8.   function doSomethingElse()
  9.   {
  10.   }
  11. }
  12.  
  13. $obj = new HelloWorld();
  14. echo $obj->sayHello()// "Hello World"

Vervolgens willen we ook enkele klassen maken welke het hallo-gezeg decoreren: een decorator om de tekst bold te maken en nog een andere om de tekst italic te maken. Het is natuurlijk mogelijk om een klasse te maken welke overerft van HelloWorld maar daar heb ik het later nog even over. Neem voor nu even aan dat overerving niet mogelijk zou zijn.

Normaal gesproken als ik de sayHello() methode zou willen uitbreiden via een Decorator patroon zou ik het volgende object maken:

PHP:
  1. class BoldHelloWorld
  2. {
  3.    var $m_object = NULL;
  4.  
  5.    function BoldHelloWorld($object)
  6.    {
  7.       $this->m_object = $object;
  8.    }
  9.  
  10.    function sayHello()
  11.    {
  12.       return "<b>".$this->m_object->sayHello()."</b>";
  13.    }
  14.  
  15.    function doSomethingElse()
  16.    {
  17.       return $this->m_object->doSomethingElse();
  18.    }
  19. }
  20.  
  21. $obj = new BoldHelloWorld(new HelloWorld());
  22. echo $obj->sayHello(); // "<b>HelloWorld</b>"

Dit zou keurig werken maar er zit een probleempje in het gebruik van de doSomethingElse() methode. Nouja, niet echt een probleem, het is alleen zo dat om de interface van het te decoreren object niet te veranderen, ik deze methode moet definieren in de nieuwe klasse zodat de aanroep wordt geforward naar het originele object. Naarmate je meer methodes hebt zul je meer code moeten toevoegen om al die methodes te forwarden. Hetzelfde probleem zou je hebben met eventuele member variabelen. Die zouden ook op de zelfde manier geforward moeten worden (ik weet het, public member variabelen zijn Evil, maar soms is de klasse die je gebruikt niet door jou ontwikkeld en zul je er gewoon omheen moeten werken).

PHP5 biedt dus de nodige features om dit probleem op te lossen, en het is nog makkelijk ook. De magic methods, __call, __get en __set stellen ons in staat om een generieke basis klasse die dit forwarding probleem op lost te bouwen (de credits voor deze klasse zijn voor mijn collega's Martin en Peter).

PHP:
  1. /**
  2.    AutoForward baseclass for automatic forwarding of
  3.    method calls and member variables.
  4.    
  5.    @author Peter C. Verhage
  6.    @author Martin Roest
  7.   */
  8. class AutoForward
  9. {
  10.   var $m_object;
  11.  
  12.   /**
  13.     Constructor.
  14.    
  15.     @param Object $object
  16.    */
  17.   function __construct(&$object)
  18.   {
  19.     $this->m_object = $object;
  20.   }
  21.  
  22.   /**
  23.     Returns the forwarded object.
  24.    */
  25.   function &__getObject()
  26.   {
  27.     return $this->m_object;
  28.   }
  29.  
  30.   /**
  31.     Forward method calls.
  32.    
  33.     @param String $method method name
  34.     @param Array $args method arguments
  35.     @return Unknown method return value
  36.    */
  37.   function __call($method, $args)
  38.   {
  39.     return call_user_func_array(array($this->m_object, $method), $args);
  40.   }
  41.  
  42.   /**
  43.     Forward property set.
  44.    
  45.     @param String $name property name
  46.     @param Unknown $value property value
  47.    */
  48.   function __set($name, $value)
  49.   {
  50.     $this->m_object->$name = $value;
  51.   }
  52.  
  53.   /**
  54.     Forward property get.
  55.    
  56.     @param String $name, property name
  57.     @return Unknown
  58.    */
  59.   function __get($name)
  60.   {
  61.     return $this->m_object->$name;
  62.   }
  63. }

Door de zogenaamde magic methodes __set, __get en _call te overriden worden alle methoden van de AutoForward klasse direct geforward naar het interne object in m_object.

Laten we deze klasse gebruiken om onze Bold en Italic decorators te ontwikkelen:

PHP:
  1. class BoldHelloWorld extends AutoForward
  2. {
  3.   function sayHello()
  4.   {
  5.     return "<b>".$this->m_object->sayHello()."</b>";
  6.   }
  7. }
  8.  
  9. class ItalicHelloWorld extends AutoForward
  10. {
  11.   function sayHello()
  12.   {
  13.     return "<i>".$this->m_object->sayHello()."</i>";
  14.   }
  15. }
  16.  
  17. $obj = new ItalicHelloWorld(new HelloWorld());
  18. echo $obj->sayHello(); // "<i>Hello World</i>"

Nu hoeven we niet langer ook de doSomethingElse methode definieren! De aanroep wordt door de AutoForward klasse automatisch geforward naar het originele object. Een object van het type ItalicHelloWorld zal identiek reageren als een HelloWorld object. Een aanroeper zou geen verschil merken, buiten dan misschien het andere resultaat.

Ik heb beloofd uit te leggen waarom overerving geen oplossing zou zijn. Het zit hem in de ItalicHelloWorld en BoldHelloWorld klassen. Wat nu als je een resultaat wilde hebben dat zowel Bold als Italic was? Welke van de twee zou je gebruiken? PHP ondersteunt geen Multiple Inheritance dus je zou aardig vast lopen. Maar niet met de AutoForward klasse! Met ons systeem zou het simpelweg een kwestie zijn van:

PHP:
  1. $obj = new BoldHelloWorld(
  2.          new ItalicHelloWorld (
  3.            new HelloWorld()
  4.          )
  5. );
  6.  
  7. echo $obj->sayHello(); // "<b><i>Hello World</i></b>"

Op deze manier hebben we helemaal geen Multiple Inheritance nodig!

Magic methods en Aspect Oriented Programming

Deze techniek is ook erg handig als je een fan bent van het zogenaamde Aspect Oriented Programming. Je zou een object kunnen hebben dat een bepaald aspect implementeert dat van toepassing is op het onderliggende object.

In plaats van een enkele methode te overriden kun je __call overriden en een daar een aspect implementeren. Laten we security als voorbeeld nemen. Stel we hebben een object maar we willen de toegang tot de members beveiligen. We zouden daarvoor de volgende klasse kunnen gebruiken:

PHP:
  1. class SecurityAspect extends AutoForward
  2. {
  3.   function __call($method, $args)
  4.   {
  5.     if (isAllowed($method))
  6.     {
  7.       return call_user_func_array(array($this->m_object, $method), $args);
  8.     }
  9.     throw new Exception("Caller not allowed to execute $method on this object.");
  10.   }
  11. }
  12.  
  13. $obj = new SecurityAspect(new HelloWorld());
  14. $obj->sayHello(); // throws error if sayHello not allowed

Een ander makkelijk te implementeren aspect is caching van resource intensieve methodes. Definieer een lijst van methodes om te cachen en een member variabele om de informatie in te cachen et voila!

Conclusie

Met deze post heb ik geprobeerd om de kracht van PHP5's magic methodes te laten zien. Java heeft gelijkwaardige oplossingen door het gebruik van Reflection maar met PHP5 is het veel eenvoudiger om dergelijke constructies samen te stellen. Dit stelt je in staat om kleine patronen met weinig code te definieren. Voel je vrij om de code uit dit artikel te gebruiken!

Over de auteur

Dit artikel is een met toestemming vertaalde versie van een blogpost van Ivo Jansch. Ivo is technical director bij ibuildings.nl en werkt o.a. aan het open source Achievo ATK framework.

Vertaald door Mathieu Kooiman.

Reageer ook!

Kleine toevoeging: met de decorator pattern voorkom je dus classes als BoldAndItalicsHelloWorld. Dit concept kun je makkelijk vertalen naar het klassieke voorbeeld van "kosten berekenen" :)

Leave a comment
Line and paragraph breaks automatic, e-mail address never displayed, HTML allowed: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>