Abstrakte Klassen und Interfaces

https://bildung.social/@oerinformatik/

https://oer-informatik.de/uml-klassendiagramm-interface-abstraktion

tl/dr; (ca. 8 min Lesezeit): Ein großes Problem bei Objekt- und Klassenbeziehung ist die starke Kopplung unterschiedlicher Klassen aneinander. Der Austausch von Verhalten ist so nur mit großem Aufwand möglich. Das Konzept der Abstraktion bietet hier in Form von abstrakten Klassen und Interfaces die Möglichkeit, Komponenten lose zu koppeln. (Zuletzt geändert am 08.04.2025)

Dieser Text ist ein Teil der Infotext-Serie zu UML-Klassendiagrammen:

Abstrakte Klassen

Wozu abstrakte Klassen dienen, soll an folgendem Beispiel erläutert werden: es soll das gesamte Vermögen, das in Aktien, auf Konten und als Festgeld angelegt ist, jederzeit ausgewertet werden können. Dazu sollen die Klassen Konto, Aktien und Festgeld von einer gemeinsamen Elternklasse Geldanlage erben, in der festgelegt ist, dass wir über die Methode getAnlagenwert() immer den aktuellen Zeitwert der Geldanlage erhalten.

Superklasse Geldanlage
Superklasse Geldanlage

Da das Guthaben zum Zeitwert beim Konto schlicht über den Kontostand, beim Festgeld über die Verzinsung der Einlage und bei einer Aktie über den aktuellen Börsenwert ermittelt werden muss, kann die Methode getAnlagenwert() nicht sinnvoll zentral in Geldanlage implementiert werden. Wir können verdeutlichen, dass jede Unterklasse eine eigene Implementierung benötigt, in dem wir den Methodenrumpf leer lassen und lediglich die Signatur (also Name + Parameter) und der Rückgabewert festlegen: derlei Methoden nennt man abstrakt.

Durch die fehlende Implementierung von getAnlagenwert() ist das Verhalten einer Geldanlage nicht vollständig definiert - es können keine konkreten Instanzen (Objekte) von Geldanlage gebildet werden. Klassen wie Geldanlage, aus denen keine Instanzen gebildet werden dürfen oder sollen, werden auch als abstrakt bezeichnet.

Im UML-Klassendiagramm werden abstrakte Klassen und Methoden durch kursive Schreibweise gekennzeichnet:

Kursive Schreibweise bei Methode und Klassennamen
Kursive Schreibweise bei Methode und Klassennamen

Abstrakte Klassen dürfen auch Implementierungen enthalten (siehe Getter/Setter + Attribut name).

Die einzelnen Kindklassen müssen alle abstrakten Methode (hier: getAnlagenwert()) implementieren, damit sie selbst konkrete Klassen sind, die instanziiert werden können:

Implementierung der abstrakten Methoden in den Kindklassen
Implementierung der abstrakten Methoden in den Kindklassen

Die kursive Schreibweise kann schnell übersehen werden, daher ist es ratsam, zusätzlich das Schlüsselwort {abstract} als constraint in geschweiften Klammern unterhalb des Klassennamens zu notieren.

explizit per constraint als abstrakt gekennzeichnete Klasse
explizit per constraint als abstrakt gekennzeichnete Klasse

In der Regel werden zwar nur Klassen mit abstrakten Methoden selbst als abstrakt dargestellt, theoretisch kann jedoch jede Klasse als abstrakt gekennzeichnet werden, auch wenn sie instanziierbar wäre. Umgekehrt gilt jedoch:

Sobald eine Klasse über mindestens eine abstrakte Methode verfügt, muss sie als abstrakt gekennzeichnet werden. Das gilt auch für Klassen, die abstrakte Methoden erben: im folgenden Beispiel ist Konto abstrakt, da es die abstrakte Methode getAnlagewert() erbt und nicht selbst implementiert. Erst von den Kindklassen PrivatKonto und Geschäftskonto können Instanzen (Objekte) gebildet werden, da sie die abstrakte Methode implementieren.

Von abstrakten Klassen erbende Klassen bleiben abstrakt, wenn sie die abstrakten Methoden nicht implementieren
Von abstrakten Klassen erbende Klassen bleiben abstrakt, wenn sie die abstrakten Methoden nicht implementieren

Alle Klassen, die nicht instanziiert werden können oder sollen, werden abstrakt genannt. Alle Klassen, die instanziiert werden können oder sollen, werden konkrete Klassen genannt.

Abstrakte Klassen können alle Member und Eigenschaften enthalten, die auch konkrete Klassen enthalten können. Von Klassen, die als abstrakt gekennzeichnet wurden, können keine Instanzen gebildet werden (unabhängig davon, ob diese abstrakte Methoden enthalten).

Als Java-Quelltext sieht die abstrakte Klasse Geldanlage und die konkreten Kindklassen Festgeld und Aktie beispielsweise so aus:

package de.oerinformatik.crm;

import java.time.LocalDate;
import java.time.Period;

public abstract class Geldanlage {
    private String name;
    
    public Geldanlage(String name){
        this.setName(name);
    }

    public String getName(){
        return this.name;
    }

    public void setName(String name){
        this.name = name;
    }

    public abstract double getAnlagewert(); 
}

class Aktie extends Geldanlage{
    private int gesamtanzahl = 0;
    private double kurswert = 0;

    public Aktie(String name){
        super(name);
    }

    public double getAnlagewert(){
        return this.gesamtanzahl * this.kurswert;
    }

    public void kaufeAktie(int anzahl){
        gesamtanzahl += anzahl;
    }

    public boolean verkaufeAktie(int anzahl){
        if (anzahl>=gesamtanzahl){
            gesamtanzahl -= anzahl;
            return true;
        }
        return false;
    }

    public void setKurswert(double kurswert){
        this.kurswert = kurswert;
    }
}

class Festgeld extends Geldanlage{
    private double betrag = 0.0;
    private LocalDate startdatum;
    private LocalDate enddatum;
    private double zins;

    public Festgeld(double betrag, LocalDate startdatum, LocalDate enddatum, double zins){
        super("Festgeld");
        this.betrag = betrag;
        this.enddatum = enddatum;
        this.startdatum = startdatum;
        this.zins = zins;
    }

    public double getAnlagewert(){
        Period dauer = Period.between(startdatum, LocalDate.now());
        System.out.println("Dauer des Investments: "+dauer.toString());
        int days = dauer.getDays();
        System.out.println("Dauer in Tagen: "+Integer.toString(days));
        double virtuellesKapital = this.betrag * Math.pow((1 + this.zins),(days/360.0));
        System.out.println("Virtuelles Kapital: "+Double.toString(virtuellesKapital));
        return virtuellesKapital;
    }
}

Interfaces

Das Konzept der Abstraktion wird mit Interfaces weiterentwickelt: Interfaces stellen abstrakte Strukturen dar, die im engeren Sinn komplett frei von Implementierung sind. Im Kern legen Interfaces lediglich Methodensignaturen und Rückgabewerte fest.

Interfaces stellen Verträge zwischen Klassen dar, die festschreiben, welche Methoden eine Klasse implementieren muss. Beispielsweise legt das Interface Verzinsbar fest, dass alle Klassen, die es realisieren (implementieren), eine Methode verzinse() anbieten müssen. Greift man auf eine Klasse zu, die gegen das Interface implementiert wurde, kann man sich also sicher sein, dass diese Methode existiert.

Das Interface Verzinsbar legt fest, dass verzinse() implementiert werden muss
Das Interface Verzinsbar legt fest, dass verzinse() implementiert werden muss

Interfaces werden im UML-Diagramm wie Klassen dargestellt, die mit dem Stereotyp <<Interface>> oberhalb des Namens notiert werden. Da die Struktur abstrakt ist, wird Name und Methode i.d.R. kursiv dargestellt.

Interfaces existieren nicht in allen Programmiersprachen (daher fehlen hier z.B. Python-Beispiele). In Java sieht das Interface beispielsweise so aus:

Klassen, die die abstrakten des Interfaces implementieren, werden mit einem “Implementierungspfeil” gekennzeichnet - mit einer nicht ausgefüllten Pfeilspitze (wie bei Vererbung) in Richtung Interface, jedoch statt der durchgezogenen Linie der Vererbung mit einer gestrichelten Linie:

Die Klasse SparKonto implementiert Verzinsbar (und somit verzinse())
Die Klasse SparKonto implementiert Verzinsbar (und somit verzinse())

Sofern die Klassen die abstrakten Methoden eines Interfaces nicht selbst implementieren, sind die Klassen selbst abstrakt. Im folgenden Beispiel implementiert die Klasse SparAnlage das Interface Verzinsbar unvollständig (verzinse() bleibt abstrakt) und ist daher selbst abstrakt.

Die Klasse SparAnlage implementiert die Methode verzinse() des Interfaces nicht, bleibt also abstrakt
Die Klasse SparAnlage implementiert die Methode verzinse() des Interfaces nicht, bleibt also abstrakt

Die Grenzen zwischen Interfaces und abstrakten Klassen sind in den unterschiedlichen Programmiersprachen unterschiedlich stark ausgeprägt:

In Java gelten beispielsweise folgende Regeln:

  • Interfaces können statische Attribute enthalten

  • Interfaces können default-Implementierungen enthalten

Eine Klasse, die das Interface Verzinsbar implementiert und ein Verhalten für die darin definierte abstrakte Methode festlegt, sieht in Java beispielsweise so aus:

Benutzen von Interfaces und Implementieren von Interfaces: Dependency Inversion Principle

Interfaces werden zur losen Kopplung zwischen Klassen genutzt. Häufig hängen Klassen über Assoziationen direkt von der Implementierung anderer Klassen ab. Im folgenden Beispiel benötigt die Klasse Kunde ein Objekt vom Typ Konto, um einen Artikel zahlen zu können:

Stark gekoppelte Implementierung ohne Nutzung eines Interfaces
Stark gekoppelte Implementierung ohne Nutzung eines Interfaces

Änderungen an Konto können somit unmittelbar dazu führen, dass sich auch die Implementierung in Kunde ändern muss. Das ist insbesondere dann nicht wünschenswert, wenn sich beide in unterschiedlichen Modulen befinden (da Kunde Konto nutzt wäre es z.B. denkbar, dass sich dieses in einem Modul höheren Abstraktionsniveaus befindet).

An die Stelle der direkten Abhängigkeit tritt ein Vertrag zwischen Kunde und Konto: Die Abhängigkeit besteht darin, dass es die Methoden abbuchen() und einzahlen() geben muss: Kunde will diese Nutzen, und das Objekt, das Kunde nutzt, muss diese Methoden bereitstellen:

Die Implementierung und Nutzung des Interfaces in Standard-Notation
Die Implementierung und Nutzung des Interfaces in Standard-Notation

In diesem Diagramm ist die Abhängigkeit zwischen Kunde und Zahlbar ganz allgemein als <<use>> angegeben - Kunde nutzt das Interface. Man darf natürlich auch genauer spezifizieren, wenn bekannt ist, welcher Art die Abhängigkeit ist (ob es sich also um eine Assoziation, Aggreagrion oder Komposition handelt). In unserem Beispiel wäre also die Darstellung einer Assoziation zwischen Kunde und Zahlbar auch korrekt (und etwas präziser):

Die Nutzung des Interfaces wird hier als Assoziation dargestellt
Die Nutzung des Interfaces wird hier als Assoziation dargestellt

Das Interface sieht in Java so aus:

Die Klasse Konto müsste die beiden Methoden implentieren:

In der Klasse Kunde wird eine Instanz genutzt, die das Interface Zahlbar implementiert (also z.B. ein Konto):

Aufgerufen würde das ganze dann etwa so:

Solange das Interface unverändert bleibt, können die jeweiligen Implementierungen geändert, ja sogar getauscht werden: Kunde hängt nur noch von der abstrakten Struktur Zahlbar ab, aber nicht mehr von einer konkreten Implementierung. Realisiert werden kann diese Abhängigkeit auch über andere Implementierungen von Zahlbar.

Die Implementierung und Nutzung des Interfaces in Standard-Notation
Die Implementierung und Nutzung des Interfaces in Standard-Notation

Kunde nutzt ein Objekt vom Typ Zahlbar, um einen Artikel zu bezahlen (siehe Auszug aus der Methode bezahleRechnung()).

Lollipop- / Ball-and-Socket-Notation

Wenn der Aufbau des Interfaces nicht so wichtig ist, wird häufig die Lollipop- (oder Ball and Socket-) Notation gewählt, die nur die Schnittstelle benennt, die Member des Interface aber nicht zeigt:

Die Implementierung und Nutzung des Interfaces in Standard-Notation
Die Implementierung und Nutzung des Interfaces in Standard-Notation

Bei Kunde ist mit der stilisierten Buchse gekennzeichnet, dass ein Zahlbar-Objekt benötigt wird. Konto stellt dieses Objekt zur Verfügung, war durch den stilisierten Stecker dargestellt wird.

Java-Beispiele und Gesamtübersicht (als Big Picture)

Der Quelltext für eine Implementierung der OOP-Beispiele findet sich im Gitlab-Repository unter src/app/ ( java | python ). Das Gesamtprojekt sieht etwa so aus:

Ausschnitt einer Rechnungs- und Konten-Verwaltung als UML-Klassendiagramm
Ausschnitt einer Rechnungs- und Konten-Verwaltung als UML-Klassendiagramm

Hinweis zur Nachnutzung als Open Educational Resource (OER)

Dieser Artikel und seine Texte, Bilder, Grafiken, Code und sonstiger Inhalt sind - sofern nicht anders angegeben - lizenziert unter CC BY 4.0. Nennung gemäß TULLU-Regel bitte wie folgt: Abstrakte Klassen und Interfaces” von oer-informatik.de (H. Stein), Lizenz: CC BY 4.0. Der Artikel wurde unter https://oer-informatik.de/uml-klassendiagramm-interface-abstraktion veröffentlicht, die Quelltexte sind in weiterverarbeitbarer Form verfügbar im Repository unter https://gitlab.com/oer-informatik/uml/umlklasse. Stand: 08.04.2025.

[Kommentare zum Artikel lesen, schreiben] / [Artikel teilen] / [gitlab-Issue zum Artikel schreiben]

Kommentare gerne per Mastodon, Verbesserungsvorschläge per gitlab issue (siehe oben). Beitrag teilen: