Freitag, 10. September 2010

Favor Composition over Inheritance thoughts

Vererbung ist ein mächtiges Mittel um wiederverwendbare Software zu schreiben. Objektorientierte Programmiersprachen nutzen Vererbung, Polymorphie, Überschreiben und Überladen, um Quellcoderedundanzen (DRY) zu minimieren und das Open Closed Principle (OCP) einzuhalten.

Nachteilig bei der Vererbung ist die enge Kopplung in Klassenhierarchien und die einzuhaltenden Regeln, die das liskovische Substitutionsprinzip vorgibt. Vererbungshierarchien sind bei fehlerhaften Designentscheidungen und nicht bedachten Randbedingungen häufig fragil. Die Fragilität äußert sich durch die nicht Ersetzbarkeit von Klassen einer konkreten Klassenhierarchie und unvorhersehbaren Verhaltensweisen bei der Verwendung einer Klassenbibliothek. Erfahrene Entwickler schlagen deshalb vor, anstelle von abstrakten Basisklassen vorzugsweise Schnittstellen zu verwenden. Diese Vorgehensweise ist allerdings nicht in allen Fällen geeignet, weil abstrakte Klassen auch Implementierungen beinhalten können. Der Mix aus konkreten Methodenimplementierungen und abstrakten Methoden gleicht den Nachteil durch Vererbung Fragilität zu erzeugen wieder aus.

Die Kapselung als wesentlicher Vorteil moderner Programmiersprachen kann bei fehlerhaftem Design in einer Klassenhierachie durch Vererbung gebrochen werden. Beim Klassendesign sollte deshalb genau überlegt werden, ob eine Klasse mit final als Endpunkt einer Vererbungshierarchie deklariert wird oder ob weitere Ableitungen der Klasse geduldet werden. Der Zugriffsmodifizierer "protected" ist auf Feld- und Methodenebene dediziert zur Regelung der Sichtbarkeit nutzbar. Methoden, die mit final deklariert wurden können nicht mehr überschrieben werden und markieren somit auf der Methodenebene den Endpunkt der Vererbungshierarchie. Mittel zur Kontrolle der Vererbung von Feldern und Methoden sind in der Programmiersprache Java vorhanden und fallweise zu nutzen.

Zur Auflockerung etwas Theorie:

In der klassischen objektorientierten Lehre werden Klassenentwürfe und die einhergehenden Ableitungen als IS-A Beziehungen bezeichnet. An dieser Stelle wird die Relation zur Typbildung deutlich. Die Komposition hingegen nutzt die Delegierung und die HAS-A Beziehung als Verweis zu einem anderen Objekt. IS-A und HAS-A Beziehungen sind fest verankerte Konzepte objektorientierter Programmiersprachen.

Java EE Patterns (Komposition und Delegierung):

Im Java EE Umfeld wird clientseitig häufig das Business Delegate eingesetzt. Das Business Delegate kümmert sich um das Lookup von Enterprise JavaBeans (EJBs) und bietet  darüber hinaus weitere clientspezifische Services zur Fehlerbehandlung und Transaktionssteuerung an. Der Client in einer Java EE Anwendung nutzt das Business Delegate wie eine lokale Komponente, ohne sich mit den entfernten Methodenaufrufen beschäftigen zu müssen. 

Das serverseitige Gegenstück zum Business Delegate ist die Facade. Die Facade ist eine vereinfachte Schnittstelle zu einem komplexen Subsystem. Facaden bieten Dienste für Clients an und sind Coarse Grained designed. Transaktionen können beim Client oder an der Grenze einer Facade beginnen. Die Transaktionssteuerung ist applikationsabhängig. Wesentlich für die Facade ist, dass durch die Bündelung von Funktionalitäten weniger Netzwerkverkehr erzeugt wird. In der Praxis können Monster Facaden entstehen, die unterschiedliche fachliche Funktionalitäten bündeln. Ratsam ist in solchen Fällen nach dem Prinzip Separation of Concerns vorzugehen und je nach fachlicher Funktionalität eine zugeordnete Facaden zu erstellen. Coarse Grained bezieht sich definitiv nicht auf die Masse der Methoden in einer Facade, sondern auf die Bündelung von Methodenaufrufen in den Methoden der Facade.

Implementierungsbeispiel:

Im Blogbeitrag soll anhand einer einfachen Counter Implementierung die Fragilität  der Vererbung gezeigt werden. Zusätzlich zur Lösung durch Vererbung wird eine Komposition veranschaulicht, die im konkreten Fall der bessere Implementierungsansatz ist. Die Counter Implementierung beinhaltet ein CounterBean mit zwei nicht synchronisierten Methoden, die für die Verwendung in nebenläufigen Umgebungen erweitert werden sollen.

Schnittstelle des Counters:

public interface Counter {

    public long getCounter();
    public long increment();
}

Implementierung des Counters:

public class CounterBean implements Counter {
   
    private long counter = 0;
   
    @Override
    public long getCounter() {
       
        return counter;
    }

    @Override
    public long increment() {

        if(Long.MAX_VALUE == counter) {
            throw new IllegalStateException("counter overflow");
        }
       
        return(++counter);
    }
}

Ableitung des Counters:

public class CounterSubBean extends CounterBean {

    @Override
    public synchronized long getCounter() {
      
        return super.getCounter();
    }
  
    @Override
    public synchronized long increment() {
      
        return super.increment();
    }
}

Die Anforderung, dass der Counter in nebenläufigen Umgebungen einsetzbar ist, wird durch synchronisierte Methoden in der abgeleiteten Klasse erfüllt. Die Implementierung ist dennoch nicht stabil, weil das Gesetz der Ersetzbarkeit gebrochen wurde. Verwendet ein Programmierer die Basisklasse anstelle der Ableitung, würde er mit Recht erwarten, dass die Methoden der Basisklasse ebenfalls synchronisiert sind. Diese Annahme kann bei unzweckmäßigem Einsatz der Basisklasse in nebenläufigen Umgebungen zu schwierig findbaren Laufzeitfehlern führen.

Die im vorliegenden Fall geeignete Implementierung ist der Decorator. Der Decorator zeigt sofort an, dass die Funktionalität der Basisklasse nicht durch Vererbung erweitert wird. Der Programmierer weiß in diesem Kontext, dass sich die Basisklasse anders als die Decorator Implementierung verhält. Zweck des Decorators ist es nämlich, eine Klasse zu erweitern, ohne den Vertrag den die Basisklasse erfüllt zu verletzen.

Decorator Implementierung des Counters:

public class CounterBeanDecorator implements Counter {

    private final Counter counter;
           
    private CounterBeanDecorator(Counter counter) {
       
        this.counter = counter;
    }

    @Override
    public synchronized long getCounter() {
       
        return (counter.getCounter());
    }

    @Override
    public synchronized long increment() {
       
        return(counter.increment());
    }
}


Der Rechtshinweis des Java Blog für Clean Code Developer ist bei der Verwendung und Weiterentwicklung des Quellcodes des Blogeintrages zu beachten.