SOLID - Das Rad neu erfunden?

1. Juli 2013


In seinem Vortrag Open/Closed für Einsteiger /11010) refaktorisierte Christian Jakob ein wenig Code, um dem Open/Closed Prinzip (OCP) Genüge zu tun. Er verwendete dazu die Technik Replace Conditional with Polymorphism , die in Martin Fowlers ausgezeichnetem Buch "Refactoring" beschrieben ist. Dabei wird beispielsweise ein Switch-Case Statement in eine Klassenhierarchie übersetzt. Mir fiel dabei auf, dass für das Refactoring die Einhaltung von OCP das Ziel darstellte - aber die anderen SOLID-Prinzipien ignoriert wurden (obwohl sie teils auch erfüllt waren)! Dies möchte ich daher an dieser Stelle weiterführend kommentieren.

Polymorphic vs. Conditional

Beispielsweise wird

public decimal CalculateSalary() 
{
    switch (_employeeType) 
    {
        case PROGRAMMER:
            return getSalary();
        case CONSULTANT:
            return getSalary() * 2;
        case INTERN:
            return getSalary() / 2;
        default:
            ...
    }
}

zu:

public decimal CalculateSalary() 
{
    return _empolyee.getSalary()
}

public interface Employee { decimal getSalary(); }

// Auszug
public class Intern : Employee { 
    ... 
    public decimal getSalary() 
    {
        return _salary / 2;
    }
}

public class Consultant : Employee {...}
public class Programmer : Employee {...}

Wie man sieht ist die eigentliche Logik im Switch-Case-Statement einer einzigen Zeile gewichen. Mehrere Klassen wurden erstellt, die ein Interface implementieren. Dieses liefert abhängig vom Typen, auf dem es aufgerufen wurde, einen anderen Wert. So kann die Klasse Programmer in der Methode getSalary einfach das normale Salary zurückgeben, während der Intern beispielsweise nur die Hälfte des normalen Salärs erzeugt. Die "CalculateSalary"-Methode bekommt von alldem nichts mit. Bei der ursprünglichen Implementierung ist dies anders. In einem neuen Fall müsste das Switch-Case Statement verändert werden. Es ist also nicht geschlossen gegenüber Veränderungen - wie vom OCP gefordert.

Die einzelnen Fälle des Switch-Case-Statements wurden mit einer eigenen Klasse ersetzt. Das Open/Closed-Principle wurde dadurch umgesetzt, da das Hinzufügen eines neuen Falles - also einer neuen Klasse - die anderen Fälle nicht berührt. Fassen wir zusammen: Das Switch-Case-Statement verletzt das OCP - eine saubere Klassenhierarchie erfüllt das OCP. Diesen Punkt konnte Christian in seinem Vortrag hervorragend zeigen.

Vervollständigung

Interessant ist an diesem Refactoring, dass es - quasi durch die Hintertür - weitere SOLID-Prinzipien umsetzt - oder zumindest für später vorbereitet. Zunächst lässt sich die Testbarkeit des Codes diskutieren. Diese ist gestiegen - darauf ging Christian auch ein. Das Switch-Case-Statement erfordert einen Test mit mindestens 4 Fällen - die Klassen benötigen jeweils nur einen Test mit und einem Fall (fürs erste, bzw. für die gleiche Testabdeckung.). Vorteil ist, dass die Testfälle voneinander isoliert sind und so ein feiner granuliertes Feedback ermöglichen.

Aber welche SOLID-Prinzipien erfüllt der Code weiterhin? Wir erinnern uns: S ingle Responsibility (SRP), O pen/Closed (OCP), L iskov Substitution (LSP), I nterface Segregation (ISP), D ependency Inversion (DIP)

Wie trifft das auf den Code zu?

Zusammenfassend lässt sich also sagen: Wir haben nur auf OCP geachtet und der Code ist gesamtheitlich "solider" geworden. Tolle Sache!

Das Rad neu erfunden?

Was aber vielen nicht aufgefallen ist: Die Implementierung lässt sich aus einem viel basaleren Standpunkt betrachten. Ich brauche garnicht SOLID in die Gegend husten, wenn noch nicht die eigentliche Natur des Codes diskutiert wurde. Nicht nur ist das Switch-Case-Statement weniger "SOLID" - es ist auch nicht objektorientiert ! Ein Switch-Case Statement ist eigentlich prozedural. Eine Art Durchlauferhitzer. Oben ein Flag rein, unten eventuell ein Ergebnis raus. Wie kontert das die Objektorientierung?

In der Uni lernen wir leider immer nur, dass ein BMW ein Auto ist; dass Katze und Hund Säugetiere sind. Und damit endet die OO-Diskussion. Vererbung wird zu sehr an der "Vererbungsmetapher" aufgehalten und zu sehr auf Eltern-Kind-Relationen reduziert. Die der wichtige Teil, der aber mit OO zu tun hat, ist der Polymorphismus.

Abhängig von ihrem Typen kann die konkrete Instanz einer Klasse, bei gleichem äußerlichen Aussehen, ein anderes internes Verhalten zeigen. Das wurde im Beispiel auch umgesetzt - man bekommt einen Employee (anstatt eines TypeCodes). Welcher genau das ist, interessiert eigentlich nur am Rande. Wichtig ist nur, dass dieser die entsprechende Methode mitbringt, die aufgerufen werden soll. Durch Typing auf das Interface wird dies vom Compiler auch geprüft. **)

Fazit

Persönlich lerne ich daraus: Bevor wir über "höhere" Themen wie SOLID diskutieren, sollten wir uns an objektorientierte Grundprinzipien erinnern. Ich glaube, dass diese oft nicht richtig umgesetzt wurden und die meisten Probleme, die man mit Code hat, dadurch entstehen, dass prozedurales bzw. auch imperatives Denken die Implementierung bestimmt. Dies leite ich unter anderem daran ab, wie viele objektorientierte Programmierer Patterns wie beispielsweise das NullObjekt-Pattern als per se unsinnig empfinden.

So geht es mir übrigens auch mit Domain Driven Design . Die Idee ist vielfältig, aber die Aspekte Modellierung und Domänenorientierung sind für mich - wie im "Evans" beschrieben - fast mit OO gleichzusetzen.

Ich glaube, dass Test-Driven Development und die Funktionale Programmierung, sowie Ansprüche auf Parallelisierung in Zukunft neue Reflektion über die grundlegenden Paradigmen provozieren werden.

*) Ich finde das LSP recht schwer zu verstehen und bin für Berichtigungen dankbar, sollte ich hier Unfug geschrieben haben!

**) In dynamischen Sprachen braucht man nicht einmal ein Interface dafür.