GIT – The decorator pattern is a technique to extend the functionality of an object instance at runtime. The cleanest implementation is to wrap new object around an existing concrete instance.

$user = new InformalName(new User('Rob','Tuley'));

The decorator itself acts as a proxy to the concrete instance it contains, and by default passes through any function calls to this concrete instance. It is “transparent”. It has the opportunity to intercept and change the return values from any of these existing functions, or indeed add extra functions of its own. A barebones implementation of the proxy is:

class Proxy {
  protected $concrete;

  function __construct($concrete) {
    $this->concrete = $concrete;
  }

  function __call($method,$args) {
    $out = call_user_func_array(
              array($this->concrete,$method),$args
           );
    // support a fluent interface: if returning self,
    // return self properly wrapped in this proxy
    return $out===$this->concrete) ? $this : $out;
  }
}

This proxy object does nothing: it is transparent to method calls on the concrete object. It does not proxy public attributes but you could handle these too using __get and __set if you wished.

Using a Proxy to Decorate

An example best demonstrates the flexibility decoration by proxy objects brings. We have the business rule:

Users in a school consist of students and staff. Students should be referred to by “forename surname”. Staff should be referred to by “title surname” to students, and “forename surname” to fellow staff.

We need a User data entity to encapsulate forename/surname get/setters, and other shared attributes:

class User {
  protected $forename;
  protected $surname;

  function __construct($forename,$surname) {
    $this->forename = $forename;
    $this->surname = $surname;
  }

  function getForename() {
    return $this->forename;
  }

  function getSurname() {
    return $this->surname;
  }
}

A first stab at the business requirements might result in Student andStaff entities:

class Student extends User {
  function getName() {
    return $this->forename.' '.getSurname();
  }
}

class Staff extends Student {
  protected $title;

  function __construct($title,$forename,$surname) {
    parent::__construct($forename,$surname);
    $this->title = $title;
  }

  function getFormalName() {
    return $this->title.' '.$this->surname;
  }
}

That’s horrible. High-level code will constantly have to choose between calling getName and getFormalName (is a student or teacher logged in?): this should be handled in one place. Perhaps we need aFormalStaff object to override getName? No. That’s definitely inheritance abuse!

We might refactor to use a string name format setting, much like thedate() works:

class Staff extends Student {
  protected $fmt = 'f s';

  // ... [snip] ...

  function getName() {
    return str_replace(array('f','s','t'),
               array($this->forename,$this->surname,
                     $this->title),
               $this->fmt);
  }

  function setNameFormat($fmt) {
    $this->fmt = $fmt;
  }
}

Better, now we have a decent entity interface. We can configure the object on creation based on context (student or staff logged in) and then just callgetName() on any other point.

$user = new Staff('Mr','Joe','Bloggs');
if ($is_student_logged_in) $user->setNameFormat('t s');

But the code doesn’t feel very clean. We’ve added a lot to the Staff data object and it’s beginning to violate the SRP. We can further refactor to use the decorater pattern. We create 2 objects that can decorate the base user object, either providing a formal or informal name.

class InformalName extends Proxy {
  function getName() {
    return $this->getForename().' '.$this->getSurname();
  }
}

class FormalName extends Proxy {
  protected $title;

  function __construct($concrete,$title) {
    parent::__construct($concrete);
    $this->title = $title;
  }

  function getName() {
    return $this->title.' '.$this->getForename();
  }
}

Notice how each class in our final implementation (User,InformalName and FormalName) has a single responsibility. We havedecorated our concrete object rather than using inheritance (which can only be used once).

$user = new User('Joe','Bloggs');
if ($is_student_logged_in) {
  $user = new FormalName($user,'Mr');
} else {
  $user = new InformalName($user);
}

No Gain without Pain

Decoration is an incredibly flexible pattern. Building a library of decorators round data objects means you can build complex business objects from small simple classes. These behaviours can be re-used across entites; can be applied based on configuration directives; and are easy to test in isolation.

But, be warned, this all comes at a cost:

  • internal method calling can be unpredicable: if you are inside of a wrapped decorator that affects the result of a function call, an internal function call will be different from an external one.
  • reflection will break: you cannot typehint against a decorated object, and functions like method_exists won’t be reliable.

As always, this pattern is useful in moderation. Don’t go mad with it.

Print Friendly

Comments

comments

Bài viết liên quan