PHP-Entwurfsmuster: Null Object
So heute sind wir mal wieder am Entwurfsmustern. Also wirklich was, das ihr beim Programmieren einsetzen könnt und nicht nur eine Lebensweisheit vom Nils. Wie immer fangen wir mit einem Problem an, dass ich durch die Verwendung des Musters lösen kann.
Nehmen wir also an, wir haben einen Logger, den wir an ein Objekt ranhängen können. Wenn ich aber nichts mitloggen will, dann muss das Objekt aber trotzdem funktionieren. Natürlich fangen wir mit einer recht einfachen, aber auch doofen Lösung an.
class Object
{
public function doSth( )
{
// do sth. very special
if (!is_null( $this->logger ) {
$this->logger->log( 'done sth. very special' );
}
}
}
Das würde jetzt ohne Probleme funktionieren. Ich will mich aber in meinem Objekt vielleicht gar nicht drum kümmern müssen, ob da jetzt ein Logger initiiert wurde oder nicht. Hier kommt das Null Object in Spiel. Wir implementieren das Null Objekt immer gegen ein Interface. In unsere Fall wäre es das Logger Interface und könnte so aussehen.
class NullLogger implements Logger
{
public function log( $text )
{
}
}
Wenn ihr jetzt diesen Logger in mein Objekt injiziere, so kann ich loggen was ich will, es passiert nichts. Genau so wie ich das gerne hätte. Es ist vielleicht nicht das weltbewegenste Entwurfsmuster, aber es kann Komplexität verringern und das ist ja eines der Hauptaufgaben von schöner Programmierung. Unser fertiges Objekt könnte dann so aussehen:
class Object
{
public function __construct( Logger $logger )
{
$this->logger = $logger;
}
public function doSth( )
{
// do sth. very special
$this->logger->log( 'done sth. very special' );
}
}
Man könnte jetzt natürlich noch einen Standard Logger initialisieren, wenn keine mitgegeben wird, aber das ist Geschmackssache und kommt wohl auf das Objekt an. Wahrscheinlich verwenden viel von euch das Muster ja schon, vielleicht kannten aber ein paar den Namen noch nicht.
Man könnte auch sagen, du hast ein Dummy-Objekte oder ein Skeleton. Da ist ansich nichts verkehrtes dran. Das hätten andere Entwickler auch so gemacht. Nur: zum „Entwurfsmuster“ würde ich diese Idee nun wirklich nicht adeln.
Ein Logger ist außerdem ein schlechtes Beispiel. Normalerweise würde man bei einem Logger nur die Funktion welche den Ausgabestrom schreibt überschreiben, wie es AFAIK auch Log4PHP bzw. Log4J machen, aber nicht eine eigene Dummy-Klasse erstellen.
@Tom: In der Literatur findest du es als Entwurfsmuster wieder, deswegen bleibe ich erstmal bei der Benamung. Was du mit „der Funktion die den Ausgabestrom schreibt überschreibt“ meinst, weiß ich leider nicht, da kannst du gerne etwas ausholen. So wie ich das verstehe, macht es nämlich nicht immer Sinn, deswegen frage ich lieber noch mal nach.
Nils hat da schon Recht: Er selbst hat da garnichts geadelt, es ist faktisch als Entwurfsmuster bekannt. Ob das auch noch von der GoF kommt, oder erst später, weiß ich auch nicht.
* „Ein Logger ist außerdem ein schlechtes Beispiel. Normalerweise würde man bei einem Logger nur die Funktion welche den Ausgabestrom schreibt überschreiben, wie es AFAIK auch Log4PHP bzw. Log4J machen, aber nicht eine eigene Dummy-Klasse erstellen.“
Ausgehend vom Interface ist keine weitere Klasse bekannt. Man kommt als (zumindest wenn man logisch bleibt ;)) nicht darum herum eine eigene Dummy-Klasse zu erstellen.
Witzig gerade gestern hatte ich dieses Thema hier im Buero aufgeworfen. Mein Bauch sagte mir, dass dieses Entwurfsmuster vielleicht mit zu den am schwierigsten zu erreichenden ist.
Mal abgesehen von einfachen Beispielen wie dem Logger, der so sinnvollerweise in vielen Projekten implementiert ist, gibt es eine Menge Methoden, die Objekte zurueckgeben oder eben null.
Wollte man diese alle so refaktorisieren, dass der sie aufrufende Client Code keine null Behandlung vornehmen muss (und darum geht es ja: Client Code soll agnostisch sein), dann muss man ggf. groessere Anderungen an seiner Architektur einplanen.
Denn oft wird auf den so abgerufenen Objekten ja irgendeine Logik ausgefuehrt. Ist diese Funktionalitaet nicht sauber gekapselt (Separation of Concerns), kann es zu Fehlverhalten fuehren, wenn das Objekt auf dem gearbeitet wird, nicht die Erwartungen erfuellt. Der Client Code will ja wissen, ob seine Berechnungen und Operationen erfolgreich waren und muss entsprechend auf Misserfolg reagieren, was der vermiedenen null Behandlung recht nahe kaeme. Deswegen ist es wichtig, solche Operationen nur dort vorzunehmen, wo ihr Ergebnis auch beurteilt und mit einer korrekten Reaktion versehen werden kann.
In einem MVC Pattern ergibt sich daraus imho die Regel, dass Controller moeglichst flach sein sollten und keine Businesslogik enthalten ausser auf einer sehr abstrakten Steuerungsebene aehnlich dem Chain of Command Pattern.
Nur zur Info: Es ist kein Pattern der GoF. In der englischsprachigen Wikipedia steht aber auch noch, dass es eigentlich nur eine spezielle Form des Strategy-Pattern ist.
@KingCrunch: gehört nicht zu den GoF, eben nachgesehen
Nichtsdestotrotz ist es ein DesignPattern, auch wenn es so simpel ist, es spricht ja nichts dagegen. Das „Template Method“-Pattern ist auch simpel und würde jeder ganz natürlich verwenden, ohne daran zu denken, dass es ein Pattern ist.
@Nils was den Logger angeht: dort werden an den (unveränderten) Logger sogenannte „Appender“ gebunden. Diese besorgen steuern den Ausgabestrom. (Und natürlich gibt es einen „NullAppender“, der alle Ausgaben nach dev/null leitet.)
Siehe zum Beiuspiel hier: http://incubator.apache.org/log4php/docs/appenders.html
Oje, nach dem Mittagessen hatte meine Rechtschreibung etwas gelitten. Sorry dafür!
Jedenfalls ist hier ein Codebeispiel:
require_once dirname(‚Logger.php‘;
Logger::configure(‚appender_null.properties‘);
$logger = Logger::getRootLogger();
$logger->fatal(„Hello World!“);
Konfiguration:
log4php.appender.default = LoggerAppenderNull
log4php.rootLogger = DEBUG, default
Der Quellcode der Appender-Klasse sieht wie folgt aus:
class LoggerAppenderNull extends LoggerAppender {
/* diverse Funktionen */
/**
* Do nothing.
*
* @param LoggerLoggingEvent $event
*/
public function append(LoggerLoggingEvent $event) {
}
}
Wie man sieht: keine Dummy-Klasse, sondern es wurde lediglich die entscheidende Methode gezielt überschrieben.
@Tom: Ist auch ne Möglichkeit, ob es die sinnvollste ist müsste man wohl mal ausdiskutieren. Aber verstanden habe ich jetzt auf jeden Fall, was du meintest.
Bei diesem Titel habe ich zu aller erst an das Doctrine Null Object gedacht…:
http://www.doctrine-project.org/Doctrine_Null/1_0
und vermutet dass dieses hier mal erklärt zu bekommen…
Naja vielleicht kann mir da mal jemand weiterhelfen (warum soll das null testen mit diesem Object schneller gehen?). Gibt es hierfür auch ein Pattern-Namen?
@Daniela: Das ist gar nicht so schwer 🙂 Es wird einfacher zu testen, weil du keine Abhängigkeiten hast. Das Null Objekt sollte so gut wie keine besitzen. Eine andere Implementierung hat bestimmt mehr und muss auch korrekt initialisiert werden. Das doctrine Dingens kenne ich übrigens nicht, ist also reine Spekulation 😉
@Tom: Bei dem von dir genannten LoggerAppenderNull handelt es sich doch ebenfalls um das NullObject-Pattern? Es ist nur etwas anders umgesetzt – statt ein Interface zu implementieren, wird von einer abstrakten Klasse abgeleitet. Das Prinzip ist aber das Gleiche, es wird eine Klasse definiert, deren Objekte „nichts“ tun.
Deswegen trägt die Klasse auch den Bestandteil „Null“ im Namen. 😉