6. September 2018

WordPress OOP Development – Single Responsibility

Ein wichtiger Aspekt in der Objekt-Orientierten Programmierung ist das SOLID Paradigma. Hier ist der erste Teil zum S aus dem SOLID Paradigma in WordPress.

Single Responsibility Principle

Damit eine Applikation gut erweiterbar ist, sollten die verschiedenen Klassen nur eine Funktion erfüllen. Vielfach sieht man in den Klassen von WordPress Plugins eine Vielzahl von Funktionen drin, die so eigentlich nicht zusammen in eine Klasse sollten. Dies erschwert in der Zukunft die Erweiterung des Plugins mit neuem Code. Eine Klasse sollte im Optimalfall also nicht sowohl WordPress Actions oder Filter als auch die gewünschte Funktionalität registrieren.

Die Registrierung von Hooks wird häufig in die Klassen verlagert, in denen auch die Funktionalität implementiert ist, was dazu führt, dass der Code nicht wirklich wiederverwendbar wird. Hier ein gekürztes Code-Beispiel aus einer populären WordPress Library, deren Methode resize ich letztes mal verwenden wollte.

class ImageHelper {
    static $home_url;
    public static function init() {
        self::$home_url = get_home_url();
        add_action('delete_attachment', array(__CLASS__, 'delete_attachment'));
        add_filter('wp_generate_attachment_metadata', array(__CLASS__, 'generate_attachment_metadata'), 10, 2);
        add_filter('upload_dir', array(__CLASS__, 'add_relative_upload_dir_key'), 10, 2);
        return true;
    }
    public function resize( $src, $w, $h = 0, $crop = 'default', $force = false ) {
        $foo = self::$home_url;
        // do something
    }
}

Hier registriert der ImageHelper in der init Methode auch gleich Filter und Actions. Wenn ich jetzt die resize Methode in meinem eigenen Code brauchen möchte, gibt es für mich ein Problem: Falls ich die init Methode zuerst aufrufe, um die home_url zu setzen, registriere ich damit zwangsweise die Filter wieder. Ich könnte die home_url von aussen setzen, da die sie public ist, da keine Sichtbarkeit definiert ist. Das ist aber sehr unschön und bei mehr Setup-Code auch nicht wartbar. Guter Code sollte aber gut erweiterbar und wiederverwendbar sein. Weiter ist es auch komplett unmöglich den gleichen Code in einer Webapplikation, die nicht auf WordPress basiert zu verwenden. Das Ziel ist die Abhängigkeiten zwischen den einzelnen Modulen möglichst gering zu halten.

Besser wäre es also, wenn die Klasse ohne Integration in WordPress auskommen würde und die WordPress Integration in einer separaten Klasse passiert. Darum würde das folgende Refactoring Sinn machen:

class ImageHelper {
    private static $home_url;
    public static function init($home_url) {
        self::$home_url = $home_url;
        return true;
    }
    public function resize( $src, $w, $h = 0, $crop = 'default', $force = false ) {
        $foo = self::$home_url;
        // do something
    }
}

class WPUploadIntegration {
    private $image_helper;
    public function __construct( ImageHelper $image_helper ) {
        $this->image_helper = $image_helper;
    }
    public function init() {
        add_action('delete_attachment', array($this->image_helper, 'delete_attachment'));
        add_filter('wp_generate_attachment_metadata', array($this->image_helper, 'generate_attachment_metadata'), 10, 2);
        add_filter('upload_dir', array($this->image_helper, 'add_relative_upload_dir_key'), 10, 2);
    }

}

Nun ist die Klasse ImageHelper erstens unabhängig von WordPress und könnte auch in einem anderen Projekt gebraucht werden (solange dort keine weiteren WordPress Funktionen aufgerufen werden), zweitens aber auch in unserem Code wiederverwendet werden. Die $home_url Variable wurde auf private gesetzt, da sie nicht von aussen gesetzt werden können sollte. Der ImageHelper wird via Dependency Injection (Objekt, d.h. instanzierte Klasse wird via Konstruktor übergeben. Mehr dazu in einem nächsten Blogpost) in die Klasse WPUploadIntegration gegeben und anschliessend die Hooks registriert.

TLDR

  • Klassen sollten ohne Abhängigkeit zu WordPress gestaltet werden falls möglich
  • Die Filter/Actions sollten in einer eigenen Klasse registriert werden, die nur für die Kopplung an WordPress zuständig ist und die eine Instanz der zu integrierenden Klasse via Dependency Injection erhält