Home > Informatik > Einführung in die OOP > 9. Vererbung > 9.3 Grundprinzipien der OOP

9.3 Grundprinzipien der OOP

Über die sogenannten Grundprinzipien der Objektorientierten Programmierung besteht in der Literatur und im Internet keine Einigkeit. Eine erste Recherche im Internet liefert folgendes Ergebnis (Google-KI):

9.3.1 Die 4 Grundsätze der OOP (Google-KI)

  1. Abstraktion: Das Ausblenden unwichtiger Details, um sich auf das Wesentliche eines Objekts zu konzentrieren. Es wird modelliert, welche Eigenschaften und Aktionen für das Programm relevant sind.
  2. Kapselung: Daten und Methoden werden in einer Klasse zusammengefasst und vor unbefugtem Zugriff geschützt (Information Hiding). Der Zugriff erfolgt ausschließlich über definierte Schnittstellen (Getter/Setter).
  3. Vererbung: Bereits existierende Klassen (Elternklassen) können als Vorlage für neue Klassen (Kindklassen) dienen. Dies fördert die Wiederverwendbarkeit von Code, da Attribute und Methoden nicht mehrfach geschrieben werden müssen.
  4. Polymorphismus: Objekte verhalten sich je nach Kontext unterschiedlich. Eine Methode kann in verschiedenen Klassen den gleichen Namen haben, aber jeweils eine andere spezifische Funktion ausführen.

Die Google-KI gibt als Quelle für diese Auflistung die Webseite StudySmarter.de an. In dem Buch von Lahres et al. [1] findet sich im Kapitel 2 - Die Basis der Objektorientierung - eine ähnliche Auflistung:

  1. Strukturierte Programmierung als Vorläufer der Objektorientierung
  2. Die Kapselung von Daten
  3. Polymorphie
  4. Die Vererbung

Anwendung auf das Buch-Projekt

Betrachten wir das Buch-Projekt, das wir auf den vorherigen Seiten entwickelt haben, unter diesen vier Aspekten.

Abstraktion

Die Klassen Buch, Roman, Sachbuch, Fachbuch und Lexikon blenden viele Attribute von echten Büchern aus. Lediglich die relevanten Eigenschaften werden berücksichtigt: Autor, Titel und Erscheinungsjahr bzw. Genre bei Romanen, Fachgebiet bei Sach- und Fachbüchern, Artikelzahl bei Lexika und Niveau bei Fachbüchern,.

Kapselung

Die relevanten Attribute werden mithilfe privater Instanzvariablen dargestellt. Von außen - von anderen Klassen - kann auf diese Instanzvariablen nur mithilfe von Getter- und Setter-Methoden zugegriffen werden. Mit setAutor(String autor) kann man beispielsweise den Autor eines Buches verändern.

Vererbung

Die bereits existierende Klasse Buch dient als Vorlage für die beiden Unterklassen Roman und Sachbuch. Die Klasse Sachbuch wiederum dient als Vorlage für die Unterklassen Lexikon und Fachbuch.

Polymorphismus

In dem ArrayList-Objekt liste der Klasse Buchliste werden Objekte verschiedener Klassen gespeichert: Buch, Roman, Sachbuch, Fachbuch und Lexikon. Jede dieser Klassen hat eine eigene Methode zeigeSpezifischeDaten(). Bei der Klasse Buch ist diese Methode leer, während sie bei den Unterklassen die spezifischen Attribute (Genre, Fachgebiet, Seitenzahl) in der Konsole ausgibt.

Die zeige()-Methode der Klasse Buchliste geht die gesamte ArrayList durch, und während der Laufzeit wird dann die jeweils passende zeigeSpezifischeDaten()-Methode ausgeführt. Wenn das aktuelle Objekt ein Roman ist, wird Roman.zeigeSpezifischeDaten() aufgerufen, handelt es sich um ein Lexikon, wird Lexikon.zeigeSpezifischeDaten() ausgeführt. Die Methode zeigeSpezifischeDaten() hat in diesen verschiedenen Klassen also den gleichen Namen und die gleiche Signatur, führt aber jeweils eine andere spezifische Funktion aus.

Fazit

Die Klassen des Buch-Projektes halten sich an diese vier Grundsätze der OOP.

9.3.2 Die Prinzipien des objektorientierten Entwurfs

In dem Buch von Lahres et al. ("Objektorientierte Programmierung: Das umfassende Handbuch", Rheinwerk-Verlag 2021) werden die in 9.3.1 dargestellten Prinzipien als "Basis der Objektorientierung" bezeichnet. In seinem Buch stellt er im nächsten Kapitel andere, stark erweiterte "Prinzipien des objektorientierten Entwurfs" auf:

  1. Prinzip der alleinigen Verantwortung
  2. Trennung der Anliegen
  3. Wiederholungen vermeiden
  4. Offen für Erweiterungen, geschlossen für Änderungen
  5. Trennung der Schnittstelle von der Implementierung
  6. Umkehr der Abhängigkeiten
  7. Mach es testbar

Diese Prinzipien sind stark angelehnt an die im Buch von Robert C. Martin ("Agile Software Development, Principles, Patterns und Practices", Pearson-Verlag 2014) dargelegten Prinzipien der OOP, die hier nur kurz aufgelistet werden sollen:

  1. The Single Responsibility Principle (1. bei Lahres) - S
  2. The Open-Closed-Principle (4. bei Lahres) - O
  3. The Liskov Substitution Principle - L
  4. The Interface-Segregation Principle (5. bei Lahres) - I
  5. The Dependency-Inversion Principle (6. bei Lahres) - D

In der Fachwelt sind diese fünf Prinzipien unter dem abkürzenden Begriff SOLID bekannt.

Die Prinzipien der OOP

Eine ausführlichere Darstellung finden Sie in der Abteilung "Begriffe und Konzepte der OOP" auf dieser Homepage. Hier wird auf jedes der sieben Prinzipien näher eingegangen. Die Ausführungen orientieren sich relativ stark an dem Buch von Lahres.

Anwendung auf das Buch-Projekt

Betrachten wir wieder das Buch-Projekt und überprüfen, inwieweit diese fünf SOLID-Prinzipien eingehalten wurden. Diese umfassende Aufgabe wurde an ChatGPT übertragen. Die Antwort der KI wird im folgenden Kasten zusammengefasst. Der Text wurde von mir stark gekürzt und an einigen Stellen korrigiert, umformuliert und teils auch ergänzt.

ChatGPT beurteilt das Buch-Projekt

Die fünf SOLID-Prinzipien von Robert C. Martin lassen sich auf das Buchprojekt recht gut anwenden. Allerdings muss man berücksichtigen, dass das Projekt bewusst didaktisch vereinfacht wurde und sich an Studienanfänger richtet. Einige Prinzipien werden daher nur teilweise oder in vereinfachter Form umgesetzt.

Single Responsibility Principle (SRP)

"Eine Klasse sollte nur genau einen Grund zur Änderung haben."

Dieses Prinzip wird im Buch-Projekt überwiegend gut eingehalten. Die Klasse Buch ist hauptsächlich für die Verwaltung allgemeiner Buchdaten zuständig (Titel, Autor, Jahr, Ausgabe der Daten). Die Unterklassen Roman, Sachbuch, Lehrbuch und Lexikon erweitern diese Aufgabe jeweils um eigene fachliche Aspekte. Dadurch besitzen die Klassen jeweils eine klar erkennbare Verantwortung.

Nicht ganz ideal ist allerdings, dass die Klassen gleichzeitig Daten speichern und die Ausgabe auf der Konsole übernehmen.

Open-Closed Principle (OCP)

"Software soll offen für Erweiterungen, aber geschlossen für Änderungen sein."

Dieses Prinzip wird im Buchprojekt besonders gut demonstriert. Die Oberklasse Buch kann durch neue Unterklassen erweitert werden ("offen für Erweiterungen"), ohne dass vorhandener Quelltext geändert werden muss ("geschlossen für Änderungen"). Neue Funktionalität wird also durch Erweiterung und nicht durch ständiges Umschreiben bestehender Klassen ergänzt.

Liskov Substitution Principle (LSP)

"Objekte einer Unterklasse müssen sich überall dort verwenden lassen, wo Objekte der Oberklasse erwartet werden."

Dieses Prinzip wird im Buchprojekt ebenfalls gut eingehalten. Ein Roman ist immer auch ein Buch, ein Sachbuch ebenfalls, und ein Lexikon ist ebenfalls ein Sachbuch und damit indirekt auch ein Buch.

Daher funktionieren Konstruktionen wie:

ArrayList<Buch> liste;

obwohl sich darin unterschiedliche Unterklassen befinden. Auch Methodenaufrufe wie

b.zeige();

funktionieren polymorph korrekt.

Die Unterklassen verletzen die Erwartungen der Oberklasse nicht: Sie entfernen keine wichtigen Eigenschaften, sie verändern die grundlegende Bedeutung der Methoden nicht und sie schränken deren Benutzbarkeit nicht ein.

Dependency Inversion Principle (DIP)

"Module sollen von Abstraktionen abhängen, nicht von konkreten Implementierungen."

Dieses Prinzip wird im Buchprojekt nur teilweise eingehalten. Positiv ist, dass die Buchliste mit der Oberklasse Buch arbeitet und nicht direkt mit Roman oder Sachbuch, dadurch entsteht bereits eine gewisse Abstraktion.

Allerdings verwendet das Projekt noch konkrete Klassen wie ArrayList, konkrete Buch-Unterklassen und direkte System.out-Ausgaben. Außerdem existieren keine Interfaces oder abstrakte Methoden.

Für Studienanfänger wäre das aber vermutlich unnötig komplex. Das Projekt zeigt bereits die Grundidee von Abstraktion, setzt das DIP aber noch nicht vollständig um.

Interface Segregation Principle (ISP)

"Klassen sollen nicht von Methoden abhängig sein, die sie nicht benötigen."

Dieses Prinzip spielt im aktuellen Buchprojekt kaum eine Rolle. Der Hauptgrund: Es werden bislang keine Interfaces verwendet.

Wir sehen also, dass das Buch-Projekt bereits einige wichtige OOP-Prinzipien umsetzt. Abstrakte Klassen und Interfaces werden in den folgenden Abschnitten behandelt, und auf die Grundprinzipien der OOP werden Sie in den höheren Semestern mit Sicherheit noch sehr intensiv eingehen.

Das oben genannte Buch von Lahres aus dem Rheinwerk-Verlag kann ich nur empfehlen, und wenn Sie Wert auf SOLIDes Programmieren legen, sollten Sie sich auch das berühmte Buch von Robert C. Martin anschaffen, allerdings nicht "Agile Software Development", sondern "Clean Code - A Handbook of Agile Software Craftsmanship". Die Neuauflage dieses Buches ist 2026 erschienen und damit ganz aktuell.

9.3.3 Unterklassen erben die Spezifikation ihrer Oberklassen

Die gleiche Überschrift findet sich in dem Buch von Lahres et al. Wir wollen an dieser Stelle näher auf das Thema eingehen.

Eine Unterklasse wie Roman ist an die Spezifikation der Oberklasse Buch gebunden, kann sie aber auf bestimmte Weise modifizieren.

Damit wir den Begriff Spezifikation verstehen können, müssen wir uns zunächst mit dem Begriff Schnittstelle auseinandersetzen, denn die Spezifikation einer Klasse enthält u.a. deren Schnittstelle.

Schnittstelle

Die Schnittstelle einer Klasse ist die sichtbare "Außenseite" der Klasse. Sie umfasst

  • die öffentlichen Instanzvariablen und Konstanten sowie
  • die öffentlichen Methoden
Beispiel

Betrachten wir die Schnittstelle der Klasse Queue (Schlange, Warteschlange):

public class Queue
{
   public Queue()
   public void enqueue(Object element)
   public void dequeue()
   public Object front()
   public boolean empty()
}

Die Schnittstelle besteht aus fünf Methoden, dem Konstruktor, einer Methode enqueue(), der ein Objekt der Klasse Object übergeben werden kann, einer Methode dequeue(), einer Methode front(), die ein Objekt zurückliefert sowie einer Methode empty(), die einen Wahrheitswert zurückliefert.

Aus der Schnittstelle kann man nicht entnehmen, wie die einzelnen Methoden intern arbeiten. Was macht enqueue(Object element) genau mit dem hinzugefügten Element? Wird es vorne in eine Liste eingefügt oder hinten? Oder wird es nach einem bestimmten Ordnungsprinzip einsortiert? Besteht die Queue überhaupt aus einer Datenstruktur wie einer Liste oder wurde sie mithilfe eines Arrays implementiert? All das geht aus der Schnittstelle nicht hervor.

Spezifikation

Die Spezifikation einer Klasse umfasst die Schnittstelle sowie weitere Informationen. In der Spezifikation wird insbesondere das Verhalten der Klasse beschrieben. Bei unserem Queue-Beispiel könnte diese Beschreibung zum Beispiel so aussehen:

public Queue()

Erzeugt eine leere Warteschlange.

public void enqueue(Object element)

Hängt das Objekt element an das Ende der Warteschlange an. Falls die Schlange leer ist, wird element als erstes Element hinzugefügt.

public void dequeue()

Entfernt das Objekt am Anfang der Warteschlange, falls die Warteschlange nicht leer ist. Ist die Warteschlange leer, bleibt sie unverändert.

public Object front()

Liefert das erste Objekt der Schlange zurück, falls diese nicht leer ist. Ansonsten wird null zurückgeliefert. Das Objekt wird aber nicht entfernt.

public boolean empty()

Liefert true zurück, wenn die Schlange kein Element besitzt, andernfalls false.

Vorbedingungen, Nachbedingungen und Invarianten

Die Spezifikation einer Klasse umfasst aber noch weitere Aspekte als eine verbale Beschreibung der Methoden. Oft werden auch die Vorbedingungen, Nachbedingungen und Invarianten angegeben.

Vorbedingungen

Eine Vorbedingung beschreibt, was vor dem Aufruf einer Methode gelten muss. Bei der Methode front() könnte zum Beispiel die Vorbedingung lauten: "Die Warteschlange darf nicht leer sein". Dann muss der Benutzer der Klasse vorher mit empty() prüfen, ob ein Element vorhanden ist.

Nachbedingungen

Eine Nachbedingung beschreibt, was nach dem Aufruf einer Methode gelten muss. Bei enqueue(Object element) könnte die Nachbedingung lauten: "Das übergebene Objekt befindet sich anschließend am Ende der Warteschlange." Bei dequeue() könnte die Nachbedingung lauten: "Das bisher erste Element der Warteschlange wurde entfernt, sofern die Warteschlange nicht leer war."

Invarianten

Eine Invariante beschreibt eine Eigenschaft, die für ein Objekt dauerhaft gelten muss, also vor und nach jedem Methodenaufruf. Bei einer Warteschlange gilt zum Beispiel: "Die Elemente werden nach dem FIFO-Prinzip verwaltet." Das bedeutet: Das Element, das zuerst eingefügt wurde, wird auch zuerst wieder entfernt (First in, first out).

Anwendung auf das Buch-Projekt

Diese Überlegungen lassen sich auch auf unser Buch-Projekt übertragen. Die Oberklasse Buch definiert eine bestimmte Schnittstelle und Spezifikation, einschließlich Methoden wie zeige(), Getter- und Setter-Methoden. Die Unterklassen Roman, Sachbuch, Lehrbuch und Lexikon übernehmen diese Spezifikation zunächst vollständig. Ein Objekt der Klasse Roman kann also überall dort verwendet werden, wo ein Objekt der Klasse Buch erwartet wird.

Aus diesem Grund dürfen Unterklassen die Bedeutung der von der Oberklasse geerbten Methoden nicht beliebig verändern. Wenn die Methode zeige() in der Oberklasse Buch bestimmte Buchdaten ausgibt, sollte zeige() in der Unterklasse Roman ebenfalls Informationen über das Buch ausgeben und nicht plötzlich eine völlig andere Aufgabe übernehmen.

Ebenso müssen die Vor- und Nachbedingungen der Oberklasse eingehalten werden. Ist beispielsweise in der Klasse Buch festgelegt, dass der Titel eines Buches niemals null sein darf, gilt diese Bedingung auch für alle Unterklassen.

Aufgabe

Diese Aufgabe wurde dem Buch von Lahres et al. entnommen.

Was halten Sie von den beiden folgenden Klassen?

class A 
{
   void machWas() 
   {
      System.out.println("Ich mach ja schon"); 
   }
}
class B extends A 
{
    void machWas()
    {
      throw new RuntimeException("nix mach ich"); 
    }
}

Das Beispiel soll demonstrieren, dass B keine echte Unterklasse von A ist, obwohl dies durch das Schlüsselwort extends ja angedeutet wird. Es liegt vielmehr ein fehlerhaftes Design vor. B ist keine echte Unterklasse von A, sondern erweitert A nur rein technisch. Das wichtige Prinzip der Ersetzbarkeit wird hier nicht eingehalten.

9.4.3 Das Prinzip der Ersetzbarkeit

Grundsätzliches

Dieses wichtige Prinzip der OOP besagt, "dass jedes Exemplar einer Klasse deren Spezifikation erfüllen muss. Das gilt auch dann, wenn das Objekt ein Exemplar einer Unterklasse der spezifizierten Klasse ist" (Lahres et al.)

Auf unser Buch-Projekt bezogen heißt das: Überall dort, wo in der ArrayList<Buch> ein Buch-Objekt stehen kann, kann auch ein Roman-, Sachbuch-, Fachbuch- oder Lexikon-Objekt stehen.

An der zeige()-Methode der Klasse Buchliste wird dies besonders deutlich.

    public void zeige()
    {
        for (Buch b : liste)
            b.zeige();
    }

Jedes Objekt der oben genannten Buch-Klassen ruft zunächst die zeige()-Methode der Oberklasse Buch auf, die dann die jeweils passende zeigeSpezifischeDaten()-Methode der einzelnen Roman-, Sachbuch-, Fachbuch- oder Lexikon-Objekte aufruft.

Hier ein Zitat aus dem Lahres-Buch, Sie müssen hier gedanklich das Wort "Steuerelement" durch das Wort "Buch" ersetzen:

"Überall dort, wo in unserer Anwendung ein Exemplar der Klasse Steuerelement erwartet wird, kann man Exemplare der Unterklassen verwenden, denn die Unterklassen erben alle Eigenschaften, die Funktionalität, die Beziehungen und die Verantwortlichkeiten der Oberklasse.

Damit sind die Exemplare der Unterklasse gleichzeitig Exemplare der Oberklasse in Bezug auf die der Oberklasse zugrunde liegende Spezifikation."

Und noch ein sehr wichtiges Zitat aus diesem Buch:

"Ein nutzendes Modul darf sich nie auf Implementierungen des genutzten Moduls verlassen, sondern immer nur auf dessen Spezifikation."

Das machen wir uns einmal an einem eigenen Beispiel klar.

Beispiel Klasse List

Sie haben eine Klasse List geschrieben, in der Methoden wie toFirst(), toLast(), getFirst(), getLast() etc. vorkommen. Als fortgeschrittener Entwickler haben Sie die Knoten der dynamischen Liste natürlich doppelt verkettet, jeder Knoten hat also einen Vorgänger (prev) und einen Nachfolger (next). Um sich die Entwicklung der Liste etwas zu vereinfachen, haben Sie die Instanzvariablen prev, next und content jedoch nicht als private deklariert, sondern als public. So können Sie einfachen Code verwenden wie beispielsweise

if (aktuell.next != null) ...

Ein anderer Entwickler aus Ihrem Team will nun eine Methode schreiben, welche die Liste rückwärts ausgibt:

public void rueckwaertsAusgeben(List liste)
{
   ListNode knoten = liste.last;

   while (knoten != null)
   {
      System.out.println(knoten.content);
      knoten = knoten.prev;
   }
}

Das funktioniert zunächst wunderbar - bis Sie auf die Idee kommen, die Implementierung der Liste zu verändern. Statt einer doppelt verketteten dynamischen Liste wählen Sie jetzt ein einfaches Object-Array zum Speichern der Listenelemente. Sie halten sich natürlich an die zuvor erstellte Spezifikation der Klasse List und implementieren die Methoden toFirst(), toLast() etc. weiterhin so, dass sie sich wie beschrieben verhalten: toFirst() markiert das erste Element als aktuell, toLast() das letzte Element und so weiter.

Das Teammitglied, das Ihre Klasse List benutzt, versteht plötzlich die Welt nicht mehr - nichts mehr funktioniert. Bereits die erste Zeile sein Methoden rueckwaertsAusgeben() bringt sein Programm zum Absturz; eine Klasse ListNode gibt es nicht mehr (die hätte in der vorherigen Version auch eigentlich private sein oder als innere Klasse vorliegen müssen), und auch der Zugriff auf liste.last führt zu einem Fehler.

Die von Ihrem Teammitglied erstellte Klasse funktioniert nicht mehr, weil sie eine bestimmte interne Implementierung der Klasse List voraussetzt: Doppelt verkettet mit first-, last-, next- und prev-Zeigern.

Genau das verletzt das Prinzip der Ersetzbarkeit: Ein Objekt muss austauschbar bleiben, solange seine öffentliche Spezifikation gleich bleibt.

Und das erleichtert den Entwicklern auch ihre Arbeit erheblich. Sie erstellen eine Klasse zunächst mit einer bestimmten Implementierung, ändern bei Bedarf die Art und Weise der Implementierung aber ab, um die Software zu optimieren. Auf die Spezifikation darf sich eine solche Veränderung der Implementierung aber nicht auswirken, weil sonst das Prinzip der Ersetzbarkeit verletzt wird.

Die nutzende Methode bzw. die gesamte nutzende Klasse darf ausschließlich die offiziellen und in der Spezifikation dokumentierten Methoden verwenden. Die Methode rueckwaertsAusgeben() hätte so implementiert werden müssen:

public void rueckwaertsAusgeben(List liste)
{
   liste.toLast();

   while (liste.hasAccess())
   {
      System.out.println(liste.getContent());
      liste.previous();
   }
}

Diese Methode funktioniert nun unabhängig davon, ob die Liste einfach oder doppelt verkettet ist, ob sie intern ein Array oder eine ArrayList benutzt oder sogar völlig anderes implementiert wurde.

Man könnte das Prinzip der Ersetzbarkeit auch zugespitzt so formulieren:

Man programmiert stets gegen die Spezifikation - niemals gegen die konkrete Implementierung.

Konsequenzen des Prinzips der Ersetzbarkeit

Aufgabe

Lahres zieht in seinem Buch drei Konsequenzen aus dem Prinzip der Ersetzbarkeit:

Erste Konsequenz

"Eine Unterklasse kann die Vorbedingungen für eine Operation, die durch die Oberklasse definiert werden, einhalten oder abschwächen. Sie darf die Vorbedingung aber nicht verschärfen."

Zweite Konsequenz

"Eine Unterklasse kann die Nachbedingungen für eine Operation, die durch die Oberklasse definiert werden, einhalten oder verschärfen. Sie darf die Nachbedingung aber nicht lockern."

Dritte Konsequenz

"Eine Unterklasse muss dafür sorgen, dass die für die Oberklasse definierten Invarianten immer gelten."

Erläutern Sie diese Aussagen und beziehen Sie sich dabei auf die Klassen aus dem Buch-Projekt sowie auf die Klasse List mit den "üblichen" Methoden wie toFirst(), toLast(), next(), prev(), append(), insert() etc.

Quellen:

  1. Lahres et al.: Objektorientierte Programmierung, Rheinwerk Computing 2021.
  2. Martin: Clean Code - A Handbook of Agile Software Craftmanship. Pearson Education 2026.

Seitenanfang
Weiter mit 9.4 - Noch ein paar Kleinigkeiten