WordPress Antipattern – Filter im Constructor zuweisen

Der Konstruktor ist eine der »magischen« Methoden einer Klasse, der in dem Moment aufgerufen wird, in dem ein neues Objekt der Klasse instanziiert wird. Ihm fällt die Aufgabe zu, den initialen Zustand des Objekts herzustellen. In der Regel werden hier dem Objekt übergebene Parameter klasseninternen Eigenschaften zugewiesen oder beeinflussen diese. Im Sinne der objektorientierten Programmierung ist es ratsam, es auch dabei zu belassen und keine sonstige Logik in den Konstruktoren zu verstecken oder ihn anderweitig zu überfrachten.

Hier und da sieht man im Design von WordPress-Plugins allerdings folgende Ansätze:

class MyPluginMetaBox {

	public function __construct() {
	
		add_action( 'add_meta_boxes', [ $this, 'addMetaBox' ] );
		add_action( 'save_post', [ $this, 'savePost' ] );
	}
	
	public function savePost( $post_id ) {
	
		// save the stuff here
	}
	
	public function addMetaBox() {
	
		add_meta_box(
			'my-meta-box-id',
			__( 'My Metabox', 'my-textdomain' ),
			[ $this, 'render_meta_box' ],
			'post',
			'normal',
			'low'
		);
	}
	
	public function render_meta_box() {
	
		// the HTML stuff
	}
}

Prinzipiell ist das in dem Beispiel gezeigte Design kein katastrophal schlechter Code, wenn sich die Klasse auf einen kleinen Bereich (z.B. eine Meta-Box) beschränkt. Im Vergleich zu immer noch weit verbreiteten »Gott«-Klassen[1], in denen sämtlicher Code eines Plugins in einer Klasse zusammengepfercht ist, ist das schon ein Schritt in Richtung »separation of concerns«. Aus Sicht der objektorientierten Programmierung handelt es sich aber um ein Anti-Pattern.

Seit wann bestimmt der Wurm, ob geangelt wird?

Eine der wesentlichen Herausforderungen bei objektorientiertem Design ist eine saubere Aufgabenteilung und eine klare Vorstellung darüber, wie sich die einzelnen Aufgaben voneinander abgrenzen. Jedes Objekt solle nur eine, möglichst spezifische Aufgabe im Programm erfüllen[2]. Die Aufgaben reichen dabei von offensichtlichen Sachen wie der Validierung von E-Mailadressen oder Datenspeicherung in einer Datenbank bis hin zur Erzeugung von Objekten – einer Aufgabe, der man sich bei funktionaler Programmierung noch nicht gegenübergestellt sah. Gerade die Objekterzeugung wird mit zunehmender Größe des Programms zu einer komplexen Aufgabe, zahlreiche Designpatterns beschäftigen sich mit diesem speziellen Thema und es existiern spezielle Frameworks für diese Aufgaben[3]. Letztlich ist auch die Zuweisung von Objektmethoden zu bestimmten WordPress Hooks eine eigenständige Aufgabe – denn ein Objekt selbst sollte über seine Verwendung in der Regel nichts wissen. So stellt man sicher, dass es nicht an einen bestimmten Kontext gebunden ist.

Die Vorteile die sich daraus ergeben liegen, zugegeben, nicht unbedingt auf der Hand – zumindest ging es mir so. Beim Wort »Wiederverwendbarkeit« dachte ich häufig daran, dass man eine Klasse, so wie sie ist, aus Projekt A nimmt, und in Projekt B und C unverändert wieder verwendet. Woher weiß ich denn heute, was ich morgen in Projekt B oder C brauche?

Wiederverwendbarkeit beginnt aber im Kleinen, noch im Programm selbst. Ein gutes Beispiel hierfür ist das WP-CLI Projekt. Das hat in den letzten Jahren eine zweite Tür zum Kern einer beliebigen WordPress-Instanz aufgestoßen. Eine gute Plugin-Implementierung, in der die Aufgaben klar verteilt sind (Datenvalidierung, Datenspeicherung,Filter-Zuweisung, usw.) erlaubt eine Erweiterung um ein CLI-Kommando, ohne dass die bestehenden Klassen geändert werden müssen. Statt dessen können die entsprechenden Klassen für Validierung und Speicherung wieder verwendet werden – weil sie sich nicht selbst an einen bestimmten Kontext wie HTTP binden.

Ein weiterer Nachteil ergibt sich für die Unit-Testbarkeit. Zwar kann ich mit Packages wie WP_Mock den add_action() Aufruf abfangen und auswerten, die übergebenen Parameter ([ $this, 'savePost' ]) kann ich aber nicht Prüfen, da das Mock vor der Instanziierung der zu testenden Klasse konfiguriert wird, die Objektinstanz also noch nicht existiert und damit auch nicht zum Vergleich herangezogen werden kann.

class MyPluginMetaBoxTest extends PHPUnit_Framework_TestCase {

	public function testConstructor() {
	
		WP_Mock::onActionAdded( 'add_meta_boxes' )
			->with( [ /* $testee, */ addMetaBox ]  );

		$testee = new MyPluginMetaBox;
	}
}

Refactor FTW

Wer »Antipattern« sagt, muss auch einen Ausweg aufzeigen. Die in dem Beispiel gezeigte Klasse krankt neben dem beschriebenen, zu aktiven Constructor noch an einem weiteren Problem: Sie ist zuständig sowohl für die Template-Generierung und Ausgabe, die Speicherung und die Registrierung der Meta-Box. Kurz um, sie macht viel zu viel. Der Fokus soll hier aber auf dem Aufräumen des Konstruktors liegen.

Bei den WordPress-Hooks handelt es sich im weiteren Sinne um Events. Ein Eventsystem besteht mindestens aus einem Emitter und einem Listener. Mit Hilfe des Emitters löst ein Trigger ein Event aus. Der Emitter propagiert das Event an die gegebenenfalls vorhandenen Listener. In WordPress gibt es keine Emitter-Instanz, die Aufgabe übernehmen dafür die Funktionen do_action() und apply_filters(). Listener werden mit den entsprechenden Gegenstücken registriert: add_action() und add_filter().

Um Emitter und Listener voneinander zu Trennen, bietet das Package league/events das ListenerProviders Interface an. Dessen bedient sich der Emitter um die Listener zu erreichen. Da WordPress nur einen »Provider« zulässt, nämlich das globale $wp_filter Array fällt eine direkte Adaption flach. Den Zwischenschritt zwischen Listener und Emitter gehen wir trotzdem:

class MetaBoxRegistrar {

	private $metaBox;
	
	public function __construct( MetaBoxInterface $metaBox ) {
	
		$this->metaBox = $metaBox;
	}
	
	public function register() {
	
		add_action( 
			'add_meta_boxes', 
			[ $this->metaBox, 'addMetaBox' ]
		);
		add_action(
			'save_post',
			[ $this->metaBox, 'savePost' ]
		);
	}
}

Das MetaBoxInterface würde sich im Wesentlichen an der eingangs gezeigten Beispielklasse orientieren und stellt sicher, dass der Registrar nicht an eine bestimmte Meta-Box gebunden ist. Es ist in der Form schon wieder ein Antipattern weil es eben Ausgabe und Speicherung in einer Klasse kombiniert und soll deshalb keine Empfehlung darstellen. Ein solcher Registrar ließe sich aber mit einem Mock der Meta-Box problemlos testen.

Kommentare

  1. Nun lag der Artikel schon etwas länger in meiner Pocket Liste ;) Allerdings erzählst du mir gerade nichts neues, denn erst vor wenige Tagen bin ich auf einen StackExchange Beitrag [1][2] gestoßen (eigentlich 2 wenn man die Referenz im 1. mit zählt), der genau das selbe sagt. Der eine Punkt der mich damals überzeugte war, dass man so viel einfacher Unit Tests schreiben kann. Das war für mich der ausschlaggebende Punkt, der mich überzeugte ;)

    [1] http://wordpress.stackexchange.com/questions/70055/best-way-to-initiate-a-class-in-a-wp-plugin
    [2] http://wordpress.stackexchange.com/questions/164121/testing-hooks-callback/164138#164138

  2. 02) 16.08.2015
    David

    Neu ist die Erkenntnis freilich nicht, ich wollte die Argumente für mich noch mal sortieren. Daher hab ich es aufgeschrieben.

Fragen, Ideen oder Kritik? – Hier ist Platz dafür!

Dein Kommentar

Um ein Kommentar abzugeben, reicht der Text im Kommentarfeld. Die Angabe eines Namens wäre nett, ist aber nicht erforderlich.

Du darfst folgenden HTML-Code verwenden, musst aber nicht:
<a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>