Das Open/Closed Prinzip. Vererbung, Delegation und Komposition.

Keylearnings:

  • Das Open/Closed Prinzip!
  • Das Prinzip der Vererbung!
  • Das Prinzip der Delegation!
  • Das Prinzip der Komposition?

Natürlich kannst du über eine rote Ampel fahren und auf das Stop-Schild und den Zebrastreifen pfeifen.

Du brauchst dich nicht in die Schlange an einer Supermarkt-Kasse zu stellen.

Zumindest dann nicht, wenn dir Ärger egal ist.

Nur wenn das jeder macht, dann gibt es Chaos!

Und genau so ist es auch beim Programmieren auch hier gibt es einige Regeln, an die du dich halten solltest. Insbesondere dann, wenn du zusammen mit Kollegen an einem größeren Projekt arbeitest.

Machst du das nicht, dann werden sie dich hassen und das mindestens ein Jahrzehnt.

Das Open/Closed Prinzip

Hast du schon mal einen Turm gebaut und anschließend versucht einen Stein in der Mitte auszutauschen ohne das der komplette Turm zusammenkracht?

Open Closed Prinzip

Ganz schön kompliziert. Nicht wahr?

Einen neuen Stein ganz oben auf den Turm zu setzen ist dagegen völlig entspannt.

Und genau so ist es auch in deinen Klassen.

Die Erweiterung einer Klasse, wie z.B. das Hinzufügen einer neuen Methode, ist in der Regel problemlos möglich. Eine Änderung kann hingegen zu einer Apokalypse führen.

Stell dir vor du hast die beste Klasse aller Zeiten mit der besten Methode aller Zeiten geschrieben.

Und weil dein Werk das Beste ist, wird es von 2424245235 Programmierern weltweit verwendet.

Doch plötzlich fällt dir ein, dass deine Methode mit drei statt zwei Parametern nochmal viel besser wäre und du änderst deine Methode entsprechend.

Was passiert?

2424245235 Programmierer laufen auf einen Fehler und hassen dich für mindestens ein Jahrzehnt.

Um genau das zu vermeiden gibt es das Open/Closed Prinzip, welches besagt, dass wir unsere Klassen zwar offen für Erweiterungen, aber geschlossen für Veränderungen halten sollen.

Die Möglichkeit Programmcode erweitern zu können ohne ihn neu schreiben zu müssen gehört schließlich zu den großen Vorteilen der objektorientierten Programmierung.

Für die Realisierung solcher Erweiterungen stehen uns drei Konzepte zur Verfügung, welche wir im folgenden besprechen wollen.

  1. Vererbung
  2. Delegation
  3. Komposition

Vererbung und Abstraktion

Bei der Vererbung beginnen wir mit einer abstrakten unkonkreten Beschreibung eines Objektes.

Was meine ich damit?

Betrachten wir ein Beispiel. Und zwar den Künstler.

Vererbung und Abstraktion

Es gibt Eigenschaft, die jeder Künstler besitzt.

So betritt jeder Künstler beispielsweise eine Bühne, die er nachdem er eine Vorstellung gegeben hat wieder verlässt.

Sowohl ein Sänger als auch ein Tänzer ist ein Künstler. Allerdings sind wir uns sicher darin einig, dass sich deren Performances auf der Bühne deutlich voneinander unterscheiden.

Wie gehen wir damit um ohne, dass wir einen Künstler doppelt programmieren müssen?

Die Lösung ist eine abstrakte Klasse Kuenstler, die wir zu einem Sänger bzw. zu einem Tänzer mit Hilfe von Vererbung verfeinern können.

Lass uns also praktisch werden und eine abstrakte Klasse Kuenstler erstellen.

1: public abstract class Kuenstler {
	
2:	public String name = null;
	
3:	public Kuenstler(String name) {
4:		this.name = name;
5:	}

6:	public void vorstellen(){
7:		System.out.println("Hallo ich bin "+name);
8:	}
	
9:	public abstract void performance();
	
10:	public void verbeugung(){
11:		System.out.println("Danke Danke!");
12:	}

13: }

Jeder Künstler hat einen Namen, den wir im Attribut name speichern. Zur Initialisierung dieses Attributs verwenden wir den Konstruktor der Klasse.

Des Weiteren haben wir die beiden Methoden verbeugung und vorstellen, also die Fähigkeiten, die jeder Künstler besitzen sollte implementiert. Innerhalb dieser Methoden erzeugen wir lediglich entsprechende Bildschirmausgaben.

Richtig interessant wird es erst in Zeile neun. Hier definieren wir die abstrakte Methode performance, d.h. die Fähigkeiten, in der sich die unterschiedlichen Künstler unterscheiden.

Da abstrakte Methoden lediglich aus einer Methodensignatur bestehen, d.h. keinen Rumpf besitzen, müssen wir die Implementierung in einer von der Kuenstler Klasse abgeleiteten Klasse durchführen.

Beginnen wir mit der Klasse Saenger.

1: public class Saenger extends Kuenstler{

2:	public Saenger(String name) {
3:		super(name);
4:	}

5:	@Override
6:	public void performance() {
7:		System.out.println("Happy birthday!");		
8:	}		
9:}

In der ersten Zeile verwenden wir das Schlüsselwort extends um unseren Sänger von der Oberklasse Kuenstler abzuleiten. Hierdurch enthält jedes Sänger-Objekt die Methoden und Attribute der abstrakten Kuenstler Klasse.

Um das Namensattribut der Kuenstler Klasse zu initialisieren verwenden wir im Saenger Konstruktor das Schlüsselwort super, mit dessen Hilfe wir den Oberklassen-Konstruktor Kuenstler(String name) aufrufen.

Der entscheidende Teil passiert in den Zeilen fünf bis acht. Hier implementieren wir die Funktionalität der performance() Methode indem wir die abstrakte Methode aus der Oberklasse überschreiben.

Da unser Sänger das Lied Happy Birthday zum besten geben möchte, erzeugt die performance() Methode die Bildschirmausgabe Happy Birthday.

Ganz ähnlich können wir die Klasse für den Tänzer implementieren.

1: public class Taenzer extends Kuenstler{

2:	public Taenzer(String name) {
3:		super(name);
4:	}

6:	@Override
7:	public void performance() {
8:		System.out.println("Yeah breakdance!");		
9:	}	
10: }

Einziger Unterschied ist die Bildschirmausgabe in der Methode performance(). Unser Tänzer singt natürlich nicht Happy Birthday sondern tritt lieber mit einer Breakdance Vorstellung auf.

Um unser Programm zu testen erstellen wir im folgenden eine Klasse mit main Methode, in der wir die Funktionen unserer Klassen der Reihe nach aufrufen.

1: public class test {

2:   public static void main(String[] args) {
		
3:      Kuenstler k = new Saenger("John");
4:	Kuenstler k2 = new Taenzer("Tim");
		
5:	k.vorstellen();
6:	k.performance();
7:	k.verbeugung();
		
8:	k2.vorstellen();
9:	k2.performance();
10:	k2.verbeugung();				
11:   }

12: }

In den Zeilen drei und vier erzeugen wir zunächst einen Sänger John und einen Tänzer Tim. Aufgrund von Polymorphie können wir sowohl John den Sänger als auch Tim den Tänzer in einer Variablen vom Typ Kuenstler speichern.

Falls du dich näher für das Thema Polymorphie interessierst, solltest du dir folgendes Video ansehen.

Im folgenden rufen wir dann der Reihe nach sämtliche Methoden der Sänger-Instanz k und der Tänzer-Instanz k2 auf.

Das Programm erzeugt die folgende Bildschirmausgabe:

1: Hallo ich bin John
2: Happy birthday!
3: Danke Danke!
4: Hallo ich bin Tim
5: Yeah breakdance
6: Danke Danke!

Da wir bei der Vorstellung und bei der Verabschiedung sowohl bei unserem Tänzer als auch bei unserem Sänger auf die Methoden der Oberklasse Kuenstler zugreifen unterscheiden sich die Ausgaben in den Zeilen 1,3,4 und 6 kaum voneinander.

Besonders ist allerdings die Ausgabe in den Zeilen zwei und fünf. Diese Ausgaben werden durch die unterschiedlichen Implementierungen der performance Methode in den Klassen Saenger und Taenzer erzeugt.

John singt Happy Birthday und Tim macht einen Breakdance!

Das Konzept der Komposition

Stell dir einen Tierpfleger vor! Ein gut qualifizierter Tierpfleger sollte sich um Hunde, Katzen, Giraffen und Stechmücken kümmern können.

Wie Programmieren wir einen Tierpfleger?

Natürlich könnten wir auch hier die Vererbung nutzen und eine abstrakte Tierpfleger-Klasse definieren und diese zu einem Hundepfleger, Katzenpfleger oder Stechmückenpfleger verfeinern.

Unter der Annahme, dass sich die verschiedenen Pfleger nur um das Attribut des Pflegetiers unterscheiden, wäre das aber viel zu aufwendig.

Stattdessen wollen wir im folgenden das Konzept der Komposition verwenden.

Bei der Komposition verschachteln wir Objekte miteinander.

So integrieren wir in der Klasse Tierpfleger ein Attribut vom Typ Pflegetier.

public class Tierpfleger {
	
	Pflegetier tier;
	
	public Tierpfleger(Pflegetier tier){
		this.tier = tier;
	}
}  

Diese Klasse enthält einen Konstruktor, mit dem wir das Pflegetier, um das sich unser Pfleger kümmern soll initialisieren können.

Doch was ist ein Pflegetier?

Ein Pflegetier könnte ein Hund, eine Katze oder eine Stechmücke sein.

Um diese Vielfältigkeit zu realisieren wollen wir die Macht von Interface Polymorphie verwenden.

Daher entwerfen wir im ersten Schritt ein Interface Pflegetier.

public interface Pflegetier {
	
	public void pflegen();
		
}

Damit wir uns auf das Wesentliche konzentrieren können, enthält unser Interface lediglich eine einzige Methode pflegen()

Als nächstes entwickeln wir zwei Klassen Hund und Katze, welche das Interface Pflegetier einbinden.

class Hund implements Pflegetier {

	@Override
	public void pflegen() {
		System.out.println("Pflege Hund");	
	}
}
public class Katze implements Pflegetier {

	@Override
	public void pflegen() {
		System.out.println("Pflege Katze");	
	}

}

Innerhalb der Klassen überschreiben wir die pflegen() Methode aus den Pflegetier Interface.

Auch hier machen wir es uns zu Demonstrationszwecken wieder ziemlich leicht und erzeugen innerhalb der Methode einfach eine Bildschirmausgabe, aus der wir erkennen können, ob eine Katze oder ein Hund gepflegt wird.

Da beide Klassen das Interface Pflegetier implementieren können wir Instanzen beider Klassen als Parameter an den Konstruktor des Tierpflegers übergeben.

Möchten wir also einen Tierpfleger erzeugen, der sich um eine Katze kümmert, so müssen wir dem Tierpfleger Konstruktor die Instanz einer Katze übergeben.

Tierpfleger katzenPfleger = new Tierpfleger(new Katze());

Wollen wir stattdessen einen Tierpfleger haben, der sich um einen Hund kümmert, so übergeben wir dem Tierpfleger-Konstruktor die Instanz eines Hundes.

Tierpfleger katzenPfleger = new Tierpfleger(new Hund());

Ganz analog könnten wir jetzt analog fortfahren um Pfleger, mit der Fähigkeit sich um Stechmücken, Elefanten und Tiger kümmern zu können zu erzeugen, indem wir einfach entsprechende Klassen für Stechmücken, Elefanten und Tiger kreieren und über den Konstruktor in die Tierpfleger-Klasse injizieren.

In unserem Beispiel haben wir uns die Kompositionen von Attributen angesehen. Darüber hinaus können wir Komposition aber auch auf das Verhalten von Objekten anwenden. Das ist die Aufgabe des sogenannten Strategy Pattern.

Das Konzept der Delegation

Wir können nicht alles! Und das ist auch nicht nötig! Es reicht wenn wir jemanden kennen, der jemanden kennt, der die Aufgabe, die wir nicht selber erledigen können für uns übernimmt.

Und genau so ist es auch bei unseren Klassen.

Es ist nicht notwendig eine eierlegende Wollmilchsau zu entwickeln. Es gibt Sachen, die unsere Klasse nicht können muss.

Mit Hilfe des Konzepts der Delegation können wir Aufgaben von einer Klasse zu einer anderen übertragen.

Ein bekanntes Beispiel in dem man das Konzept der Delegation verwendet sind Schaltflächen.

Schaltflächen kennen nur die beiden Zustände „bin gedrückt“ und „bin nicht gedrückt“ und haben außerdem die Fähigkeit anderen Objekten mitzuteilen wenn sie ihre Zustände ändern.

Diese Mitteilung an andere nennen wir ein Ereignis oder auf Englisch einen Event.

Natürlich darf deine Textverarbeitung nicht auf ein Ereignis reagieren, welches du durch einen Klick auf den Play-Button in deinem Mediaplayer erzeugst.

Daher müssen wir angeben, welche Objekte bei einem auftretenen Event benachrichtigt werden müssen.

Und das ist die Aufgabe eines sogenannten Event-Listeners.

So besitzt der Play-Button unseres Mediaplayers beispielsweise einen Listener, der auf Mausklicks auf die Schaltfläche horcht.

Natürlich hat der Play-Button nicht die Intelligenz eine Video- oder Audiodatei abzuspielen.

Allerdings können wir beim Listener ein Objekt registrieren, der diese Aufgabe übernehmen kann.

Sobald wir also auf den Play-Button drücken, wird ein Click-Event erzeugt, den der Listener abfängt und die Aufgabe „Interpretiere Multimediadatei“ an den im Listener registrierten Programmteil des Mediaplayers delegiert, der das abspielen der Multimediadatei übernehmen kann.

Delegation

Fazit: In diesem Artikel hast du das Open/Closed Prinzip kennengelernt. Dieses Prinzip besagt, dass wir unsere Klassen offen für Erweiterungen aber geschlossen für Änderungen halten sollen. Die drei Konzepte mit welchen du das Open/Closed Prinzip realisieren kannst sind Vererbung, Delegation und Komposition.

Ich freue mich über deine Fragen in den Kommentaren!

Hat dir der Artikel gefallen? Dann folge uns doch gleich auf Facebook!

Hallo ich bin Kim und ich möchte ein großer Programmierer werden. Machst du mit?

Kommentare (4)

  • Antworte

    Super Artikel! Danke für deine Hilfe, ein besserer Programmierer zu werden!

    • Hi Thomas, vielen Dank für deine nette Rückmeldung. Viele Grüße Kim

  • Antworte

    Guter Artikel mit guten Beispielen!
    Ein Code-Beispiel bei der Delegation wäre noch hilfreich.

    • Hallo Peter, danke dir! Werde da irgendwann noch was nachliefern. Viele Grüße Kim

Hinterlasse ein Kommentar