HalloPHP

Beispiel Decorator Pattern

Tutorials | letzte Änderung am 24. Juli '10 um 09:58 Uhr

Sinn und Zweck des Dekorierers

Anders als Vererbung bietet dieses Design Pattern die Möglichkeit, neue Funktionen zur Laufzeit des Programms Objekten hinzufügen zu können.

Ein möglicher Anwendungsfall

Als kleines Anwendungsbeispiel habe ich mir die Überschriften von HalloPHP gegriffen. Eine Überschrift wird also im Folgenden als ein Objekt einer Klasse betrachtet. Wie nur unschwer zu erkennen ist, sind die Anfangsbuchstaben der Überschriften etwas größer und andersfarbig. Genau diese Eigenschaft möchte ich nun den Überschriften mit dem Dekorierer-Muster hinzufügen.

Die Implementierung

Zunächst definieren wir für unsere Überschriftenklasse und unseren Dekorierer ein gemeinsames Interface, das beide implementieren sollen.

interface Headline {
  public function getText();
}

Die im Interface deklarierten Methoden können wir später über den Dekorierer erweitern. Wir beginnen nun mit dem Entwurf unserer Überschriftenklasse. Diese muss zunächst nicht mehr können, als einen Text aufnehmen und wieder zurückgeben zu können. Außerdem wollen wir bei der Initialisierung der Objektinstanz auch direkt die Überschriftenordnung mit angeben können.

<?php
class TextHeadline implements Headline {
  private 
$text;

  private 
$level;

  public function 
__construct($text$level 1) {
    
$this->text $text;

    
$level = (int)$level;
    if (
$level >= && $level <= 6) {
      
$this->level $level;
    }
    else {
      
$this->level 1;
    }
  }

  public function 
getText() {
    return 
'<h' $this->level '>' $this->text '</h' $this->level '>';
  }
}

Der nächste Schritt ist der Entwurf des Dekorierers, der ebenfalls das zuvor definierte Interface implementieren muss.

<?php
abstract class HeadlineDecorator implements Headline {
  protected 
$headline;

  public function 
__construct(Headline $headline) {
    
$this->headline $headline;
  }
  
  public function 
getText() {
    return 
$this->headline->getText();
  }
}

Diese Klasse definieren wir als abstrakt, da von ihr keine Instanzen erzeugt werden sollen. Das Dekorieren wird von konkreten Dekorierern übernommen, die wir nun von unserer Klasse ableiten.

<?php
class GreenFirstLetterDecorator extends HeadlineDecorator {
  public function 
getText() {
    
preg_match('/<h([1-6])>(.+?)<\/h\1>/'$this->headline->getText(), $match);
    
$text $match[2];

    
$first_letter substr($text01);
    
$substr substr($text1strlen($text));

    
$headline '<span class="green">' $first_letter  '</span>' $substr;

    return 
'<h' $match[1] . '>' $headline '</h' $match[1] . '>';
  }
}

class 
BlueFirstLetterDecorator extends HeadlineDecorator {
  public function 
getText() {
    
preg_match('/<h([1-6])>(.+?)<\/h\1>/'$this->headline->getText(), $match);
    
$text $match[2];

    
$first_letter substr($text01);
    
$substr substr($text1strlen($text));

    
$headline '<span class="blue">' $first_letter  '</span>' $substr;

    return 
'<h' $match[1] . '>' $headline '</h' $match[1] . '>';
  }
}

class 
OrangeFirstLetterDecorator extends HeadlineDecorator {
  public function 
getText() {
    
preg_match('/<h([1-6])>(.+?)<\/h\1>/'$this->headline->getText(), $match);
    
$text $match[2];

    
$first_letter substr($text01);
    
$substr substr($text1strlen($text));

    
$headline '<span class="orange">' $first_letter  '</span>' $substr;

    return 
'<h' $match[1] . '>' $headline '</h' $match[1] . '>';
  }
}

Jeder dieser Dekorierer färbt den Anfangsbuchstaben unserer Überschrift in einer anderen Farbe.

Der Dekorierer in der Anwendung

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
  "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
  <head>
    <title>
      Decorator Pattern
    </title>
    <style type="text/css">
      .green {
        color:#77e500;
        font-size:1.8em;
      }

      .blue {
        color:#0969bb;
        font-size:1.8em;
      }

      .orange {
        color:#ffb400;
        font-size:1.8em;
      }
    </style>
  </head>
  <body>
    <?php
    $headline 
= new TextHeadline('First letter decorator');
    
$decorator = new GreenFirstLetterDecorator($headline);
    echo 
$decorator->getText();

    
$headline = new TextHeadline('First letter decorator'2);
    
$decorator = new BlueFirstLetterDecorator($headline);
    echo 
$decorator->getText();

    
$headline = new TextHeadline('First letter decorator'3);
    
$decorator = new OrangeFirstLetterDecorator($headline);
    echo 
$decorator->getText();
    
?>
  </body>
</html>

Als Ergebnis erhalten wir folgenden HTML-Code:

<h1>
  <span class="green">F</span>irst letter decorator
</h1>
<h2>
  <span class="blue">F</span>irst letter decorator
</h2>
<h3>
  <span class="orange">F</span>irst letter decorator
</h3>

Und ab in die Produktion

Da wir natürlich mehr als nur eine Überschrift pro Seite und Typ benötigen, möchten wir die Überschriften nun gerne in einer Fabrik (engl. factory) produzieren. Zusätzlich lösen wir damit unseren Code von einer konkreten Implementierung, da die Erzeugung der Instanzen nun gekapselt in einer Klasse vorliegt und bei Veränderung an der Instanzierung, z.B. durch Ergänzung weiterer Parameter, nur Änderungen an zentraler Stelle vorgenommen werden müssen.
Um dies zu realisieren, müssen wir noch einmal unsere Überschriftenklasse überarbeiten und eine Fabrik implementieren. Die Überschriftenhierarchiestufe wird durch eine Typisierung über die Überschriftenklasse durch Vererbung festgelegt.

<?php
abstract class TextHeadline implements Headline {
  protected 
$text;

  protected 
$level;

  public function 
getText() {
    return 
'<h' $this->level '>' $this->text '</h' $this->level '>';
  }
}

class 
PageMainHeadline extends TextHeadline {
  public function 
__construct($text) {
    
$this->text $text;
    
$this->level 1;
  }
}

class 
SidebarCategoryHeadline extends TextHeadline {
  public function 
__construct($text) {
    
$this->text $text;
    
$this->level 2;
  }
}

class 
ArticleHeadline extends TextHeadline {
  
// ...
}

Beliebig viele Typen von Überschriften können so leicht hinzugefügt werden. Es folgt nun die Implementierung der Fabrik, die die Produktion unserer Überschriften übernimmt.

<?php
abstract class HeadlineFactory {
  public function 
getHeadline($text) {
    
$headline $this->createHeadline($text);

    return 
$headline;
  }

  abstract protected function 
createHeadline($text);
}

class 
PageMainHeadlineFactory extends HeadlineFactory {
  protected function 
createHeadline($text) {
    
$headline = new PageMainHeadline($text);
    return 
$headline;
  }
}

class 
SidebarCategoryHeadlineFactory extends HeadlineFactory {
  protected function 
createHeadline($text) {
    
$headline = new SidebarCategoryHeadline($text);
    return 
$headline;
  }
}

Für jeden Überschriftentyp leiten wir eine Klasse von unserer Fabrik ab, in denen dann die Instanzierung der jeweiligen Überschrift vorgenommen wird.
Am Dekorierer brauchen wir bei der gesamten Umstrukturierung nichts zu ändern. Ein einfaches Beispiel zur Verwendung könnte so aussehen:

<?php
$pageMainHeadlineFactory 
= new PageMainHeadlineFactory();
$headline $pageMainHeadlineFactory->getHeadline('First Letter Decorator');
$decorator = new GreenFirstLetterDecorator($headline);
echo 
$decorator->getText();

$sidebarCategoryHeadlineFactory = new SidebarCategoryHeadlineFactory();
$headline $sidebarCategoryHeadlineFactory->getHeadline('First Letter Decorator');
$decorator = new BlueFirstLetterDecorator($headline);
echo 
$decorator->getText();

// ...

Mit unserer Fabrik können wir jetzt kinderleicht schnell und unkompliziert weitere Überschriften festgelegter Typen und Hierarchien erzeugen.

Antworten

nikosch | verfasst am 14. Juli '10 um 18:46 Uhr

#1

Sorry, ich meckere..

Fast schjön ;) IMHO gehört - wenn Du schon HTML ausgibst (was für dieses Beispiel kaum anders geht) - dann auch das H-Tag in die Ausgabe. Ergo die Überschriften-Hierarchiestufe in den Konstruktor.
Alternativ könnte man eine Methode getClass() o.ä. einführen, das bei den dekorierten Elementen dann eben die Farbklasse liefert und alles im Template zusammenbauen.
Aber die vorliegende Kombination finde ich etwas unglücklich.

nikosch | verfasst am 14. Juli '10 um 18:52 Uhr

#2

PS:
„Ergo die Überschriften-Hierarchiestufe in den Konstruktor.“

Noch schöner wäre natürlich eine Typisierung des Headline-Objekts über seine Klasse. Also ein semantischer Ansatz:

class PageMainHeadline {}
class SidebarCategoryHeadline {}

usw. Die Vorbelegung der Hierarchiestufe könnte dann bspw. eine initialisierbare Factory übernehmen. Das ginge soweit, dass man aus dem selben Objekt dann einen Pageinhalt und einen Tickerinhalt/Feedaufreißer/Tagcloud-Eintrag/Whatever erzeugen könnte.

PPS: Auch das könnte ein Anwendungsfall sein: Ein dekoriertes PageMainHeadline-Objekt spuckt im Kontext eines Feedgenerators vielleicht ein anderes H-Tag oder ein komplett anderes XML-Tag aus. Sozusagen das Rendering als Decorator.

Asipak | verfasst am 15. Juli '10 um 10:50 Uhr

#3

Quote:
Sorry, ich meckere..

Ehrliche Kritik ist die beste Kritik.

Danke für die Tipps, ich sehe, in diesem Beispiel steckt noch eine Menge Potential.

Ich frage mich nur gerade, ob

Quote:
Das ginge soweit,

nicht genau das ist, nämlich zu viel für ein kurzes Beispiel zum Dekorierer.
Aber vermutlich liegt das nur an meinem gewählten Beispiel.

Es würde mich aber durchaus persönlich reizen, deine Vorschläge so gut es geht umzusetzen, schon alleine, weil ich dabei dann auch wieder einiges lernen kann. Das alles eine Nummer komplexer geht, sollte klar sein, doch genau das ist dann wohl die Herausforderung, die mir bevorsteht und dabei alle "Mitspieler" richtig einzusetzen.

Jedenfalls wäre das dann wohl wieder einen neuen Artikel wert :)

Asipak | verfasst am 21. Juli '10 um 11:25 Uhr

#4

Ich habe mal deinen Vorschlag mit der Verlagerung des Überschriftentags in die Klasse umgesetzt. Aufgrund dessen wird es innerhalb der Dekorierer natürlich zu einer Fummelarbeit, da ich die <h1>-Tags wieder von der Überschrift trennen muss, damit ich diese "dekorieren" kann. Ich hätte natürlich auch den Offset in der Funktion substr() anpassen können, aber über einen regulären Ausdruck schien es mir irgendwie eleganter zu sein, da ich so eine klare Trennung von Text und HTML zurückgewinne.

Natürlich hast du Recht, die Anwendung ist so auf jedenfall praktischer. Eventuell setze ich auch noch deine anderen Ideen um, wenn ich denn die Zeit dazu finde ;)

Asipak | verfasst am 24. Juli '10 um 10:04 Uhr

#5

Es hat gar nicht lange gedauert, schon ist die Fabrik fertig. Ich hoffe, du hattest das so in der Art gemeint.

Leider kann ich mir unter

Quote:
Das ginge soweit, dass man aus dem selben Objekt dann einen Pageinhalt und einen Tickerinhalt/Feedaufreißer/Tagcloud-Eintrag/Whatever erzeugen könnte.

PPS: Auch das könnte ein Anwendungsfall sein: Ein dekoriertes PageMainHeadline-Objekt spuckt im Kontext eines Feedgenerators vielleicht ein anderes H-Tag oder ein komplett anderes XML-Tag aus. Sozusagen das Rendering als Decorator.

nicht so richtig vorstellen, wie das im Code aussehen könnte. Freue mich über weitere Kritik.

[edit] Mit PPS meinst du, dass das dekorierte Objekt durch einen weiteren Dekorierer gejagt wird? Wenn ja, darüber habe ich mir auch schon Gedanken gemacht. Leider ist es in diesem Fall etwas unglücklich, HTML-Code mit in die Klasse nehmen zu müssen. Man weiß ja nicht (innerhalb des Dekorierers) wie viele Dekorierer schon über das Objekt gelaufen sind und inwiefern sich der Inhalt von $this->text bereits verändert hat. :(

Gruß