Zum Inhalt springen

12. Statik-Modellierung

Zu Zen-Modus wechseln

Bevor ein größeres Programm geschrieben wird, muss geklärt sein, aus welchen Bausteinen es besteht: Welche Klassen gibt es, welche Attribute und Methoden haben sie, und wie hängen sie zusammen? Diese Struktur heißt die Statik eines Systems, im Unterschied zur Dynamik, den Abläufen zur Laufzeit (Kapitel 13). Das Standardwerkzeug zur Darstellung der Statik ist das Klassendiagramm aus der UML (Unified Modeling Language), der genormten grafischen Notation der Softwaretechnik.

Ein Klassendiagramm hat zwei Aufgaben. Vor der Implementierung ist es ein Entwurfswerkzeug: Strukturfehler (eine unpassende Vererbung, eine fehlende Klasse) sind im Diagramm in Minuten korrigiert, im fertigen Code kostet dieselbe Korrektur Stunden. Nach der Implementierung ist es ein Kommunikationsmittel: Ein Diagramm vermittelt die Architektur eines Programms schneller als das Lesen des Quellcodes, und Entwurfsdiskussionen im Team werden über Diagramme geführt, nicht über Codezeilen.

Eine Klasse wird als Rechteck mit drei Abschnitten dargestellt: Name, Attribute, Methoden.

classDiagram
    class Account {
        -owner: str
        -balance: float
        +deposit(amount)
        +withdraw(amount)
        +balance() float
    }

Die Zeichen vor den Namen geben die Sichtbarkeit an:

SymbolSichtbarkeitBedeutung in Python
+publicnormaler Name, Teil der Schnittstelle
-privateUnterstrich-Konvention (_balance)
#protectedfür Subklassen gedacht

Datentypen von Attributen und Rückgabewerten werden nach einem Doppelpunkt notiert (balance: float). Wie viel Detail ins Diagramm gehört, richtet sich nach dem Zweck: Ein Übersichtsdiagramm der Gesamtarchitektur zeigt oft nur Klassennamen und Beziehungen; ein Detaildiagramm einer einzelnen Komponente listet Attribute und Methoden vollständig. Beide sind korrekt, sie beantworten verschiedene Fragen.

Der eigentliche Informationsgehalt eines Klassendiagramms liegt in den Verbindungslinien. Die UML unterscheidet mehrere Beziehungsarten mit jeweils eigener Notation.

Eine einfache Linie bedeutet, dass Objekte der einen Klasse Objekte der anderen kennen und verwenden. An den Linienenden stehen Multiplizitäten: wie viele Objekte der einen Seite mit wie vielen der anderen verbunden sein können.

classDiagram
    Project "1" --> "0..*" Clip : contains

Lesart: Ein Projekt enthält null bis beliebig viele Clips; jeder Clip gehört zu genau einem Projekt. Die gebräuchlichen Multiplizitäten: 1 (genau eines), 0..1 (optional), 0..* oder * (beliebig viele), 1..* (mindestens eines). Ein Pfeil an der Linie gibt die Navigationsrichtung an: Das Projekt kennt seine Clips, die Clips kennen ihr Projekt nicht.

Für Ganzes-Teil-Beziehungen gibt es zwei verschärfte Formen der Assoziation, beide mit einer Raute am Ende des Ganzen:

  • Aggregation (hohle Raute): Das Ganze fasst Teile zusammen, aber die Teile existieren auch unabhängig. Eine Playlist aggregiert Songs; wird die Playlist gelöscht, existieren die Songs weiter.
  • Komposition (gefüllte Raute): Die Teile gehören existenziell zum Ganzen. Wird ein Schnittprojekt gelöscht, verschwinden auch seine Timeline-Spuren; eine Spur ohne Projekt ergibt keinen Sinn.
classDiagram
    Playlist o-- Song : aggregation
    Project *-- Track : composition

Die Unterscheidung wirkt akademisch, hat aber eine praktische Konsequenz im Code: Bei Komposition erzeugt und verwaltet das Ganze seine Teile selbst (die Spuren entstehen in Project.__init__ oder über project.add_track()); bei Aggregation werden unabhängig existierende Objekte nur eingehängt (playlist.add(song)).

Vererbung wird als Pfeil mit hohler, geschlossener Dreiecksspitze von der Subklasse zur Basisklasse gezeichnet (Kapitel 5). Die Pfeilrichtung folgt der Sprechweise: ImageAsset --|> MediaAsset liest sich “ImageAsset ist ein MediaAsset”.

Abstrakte Klassen und Methoden (Kapitel 6) werden kursiv geschrieben; wo Kursivschrift technisch nicht verfügbar ist, dient der Stereotyp <<abstract>> als Ersatz. Reine Schnittstellen ohne jede Implementierung erhalten den Stereotyp <<interface>> und werden mit gestricheltem Vererbungspfeil realisiert:

classDiagram
    class MediaAsset {
        <<abstract>>
        +filename: str
        +size_bytes: int
        +describe()* str
        +size_megabytes() float
    }
    class Exportable {
        <<interface>>
        +export(target_path)*
    }
    MediaAsset <|-- ImageAsset
    MediaAsset <|-- VideoAsset
    Exportable <|.. ImageAsset
    Exportable <|.. VideoAsset

Das Diagramm transportiert damit auf einen Blick, was im Code über mehrere Dateien verteilt ist: MediaAsset ist nicht instanzierbar, describe() muss von jeder Subklasse implementiert werden (der Stern markiert die abstrakte Methode), und Bilder wie Videos erfüllen zusätzlich den Vertrag Exportable.

Die beiden Pfeile tragen dieselbe hohle Dreiecksspitze, unterscheiden sich aber in der Linienart, und das ist bewusst so: Die Linie steht für zwei verschiedene Beziehungen.

  • Durchgezogene Linie (<|--): Generalisierung (Vererbung). Die Subklasse erbt Struktur und Implementierung der Basisklasse. ImageAsset ist ein MediaAsset und übernimmt dessen fertigen Code, etwa size_megabytes().
  • Gestrichelte Linie (<|..): Realisierung (Implementierung eines Interfaces). Die Klasse verpflichtet sich nur auf den Vertrag, ohne Implementierung zu erben. ImageAsset sagt zu, Exportable zu erfüllen, muss export() aber selbst ausprogrammieren.

Als Merkregel: durchgezogen heißt “erbt fertigen Code”, gestrichelt heißt “sagt nur Verhalten zu”. Dieselbe Unterscheidung findet sich im Code wieder, wo eine echte Basisklasse Methoden mitbringt, ein reines Interface (<<interface>>) dagegen nur abstrakte Methoden vorschreibt (Kapitel 6).

Der Lehrplan formuliert als Kompetenz, die Struktur gegebener Problemstellungen zu analysieren und grafisch darzustellen. Das folgende Verfahren führt systematisch von einem Anforderungstext zum Klassendiagramm.

  1. Klassenkandidaten finden. Substantive des Textes sind Kandidaten: Gerät, Kamera, Mikrofon, Stativ, Person, Entlehnung. Nicht jedes Substantiv wird eine Klasse: “Inventarnummer” und “Richtcharakteristik” sind Attribute, “Medienabteilung” ist der Kontext des Systems.

  2. Gemeinsamkeiten und Hierarchie klären. Kamera, Mikrofon und Stativ teilen Inventarnummer, Anschaffungsjahr und die Verfügbarkeitslogik: eine Basisklasse Device bietet sich an. Der “ist-ein”-Test bestätigt: Eine Kamera ist ein Gerät. Da nur konkrete Geräte existieren (“ein Gerät an sich” gibt es im Verleih nicht), wird Device abstrakt.

  3. Attribute und Methoden zuordnen. Gemeinsames in die Basisklasse (inventory_number, purchase_year, is_available(date)), Spezielles in die Subklassen (sensor_resolution und mount_type zur Kamera, polar_pattern zum Mikrofon). Das Stativ hat keine Sonderattribute und bleibt eine leere Subklasse; das ist zulässig und dokumentiert den Typ.

  4. Beziehungen und Multiplizitäten festlegen. Die Entlehnung verbindet genau eine Person mit genau einem Gerät. Eine Person kann beliebig viele Entlehnungen haben, ein Gerät ebenfalls (nacheinander); die Einschränkung “nur eine gleichzeitige Entlehnung pro Gerät” ist eine zeitliche Regel und wird nicht durch Multiplizitäten, sondern durch die Logik von is_available() abgebildet. Diagramme haben Grenzen; solche Regeln notiert man als Kommentar am Diagramm.

  5. Zeichnen und gegenlesen.

    classDiagram
        class Device {
            <<abstract>>
            +inventory_number: str
            +purchase_year: int
            +is_available(date) bool
        }
        Device <|-- Camera
        Device <|-- Microphone
        Device <|-- Tripod
        class Camera {
            +sensor_resolution: str
            +mount_type: str
        }
        class Microphone {
            +polar_pattern: str
        }
        class Loan {
            +start_date: date
            +return_date: date
        }
        Person "1" --> "0..*" Loan
        Device "1" --> "0..*" Loan

Modell und Code sind zwei Darstellungen derselben Struktur; die Übersetzung muss in beide Richtungen beherrscht werden. Vom Diagramm zum Code ist sie mechanisch:

from abc import ABC, abstractmethod
class Device(ABC):
def __init__(self, inventory_number, purchase_year):
self.inventory_number = inventory_number
self.purchase_year = purchase_year
def is_available(self, date):
... # checks the loans referring to this device
class Camera(Device):
def __init__(self, inventory_number, purchase_year, sensor_resolution, mount_type):
super().__init__(inventory_number, purchase_year)
self.sensor_resolution = sensor_resolution
self.mount_type = mount_type

Die Gegenrichtung, aus bestehendem Code ein Diagramm ableiten, ist die häufigere Tätigkeit im Berufsalltag: Wer ein fremdes Projekt übernimmt, skizziert dessen Klassen und Beziehungen, um sich zu orientieren. Als Leseregeln: Jede class-Zeile ist ein Kasten, jedes class Sub(Base) ein Vererbungspfeil, jedes Attribut, das ein anderes Objekt oder eine Liste von Objekten hält, eine Assoziation (Listen deuten auf 0..*).

Klassendiagramme lassen sich zeichnen (Papier, diagrams.net) oder als Text beschreiben, aus dem ein Werkzeug die Grafik erzeugt. Alle Diagramme dieses Skriptums sind mit Mermaid als Text definiert; die Alternative PlantUML arbeitet nach demselben Prinzip. Der Quelltext des Beispiels von oben:

classDiagram
class Device {
<<abstract>>
+inventory_number: str
+is_available(date) bool
}
Device <|-- Camera
Person "1" --> "0..*" Loan

Textbasierte Diagramme haben zwei handfeste Vorteile: Sie lassen sich mit Git versionieren wie Quellcode (ein Diff zeigt, welche Beziehung sich geändert hat), und sie sind schneller zu ändern, als jedes Kästchen von Hand zu verschieben. Für Übungen und für das Abschlussprojekt sind beide Formen zugelassen; verlangt ist korrekte Notation, nicht ein bestimmtes Werkzeug.

Modelle werden wie Code begutachtet. Für die gegenseitige Durchsicht (und die eigene Kontrolle vor der Abgabe) die zentralen Prüffragen:

  • Deckt das Modell die Aufgabenstellung? Jede Anforderung des Textes muss einer Klasse, einem Attribut, einer Methode oder einer Beziehung zuordenbar sein; was sich nicht zuordnen lässt, fehlt.
  • Besteht jede Vererbung den “ist-ein”-Test und das Substitutionsprinzip (Kapitel 5)? Verdächtig sind Subklassen, die geerbte Methoden nicht sinnvoll erfüllen können.
  • Sind die Multiplizitäten begründbar? Für jedes Linienende muss ein Satz der Form “ein X hat … Y, weil …” formulierbar sein.
  • Ist die Sichtbarkeit konsistent? Interner Zustand -, Schnittstelle +; ein Diagramm, in dem alles + ist, hat über Kapselung nicht nachgedacht.
  • Trägt jede Klasse Verantwortung? Eine Klasse ohne Methoden ist oft nur ein Datenbehälter, der zu einer anderen Klasse gehört; eine Klasse mit zwanzig Methoden bündelt vermutlich mehrere Verantwortungen.

Fremde Modelle begründet zu kritisieren gehört ausdrücklich zu den Lernzielen: Die Fähigkeit zu sagen, warum eine Struktur problematisch ist (und nicht nur, dass sie einem nicht gefällt), unterscheidet ein Review von einer Geschmacksäußerung.

Lernergebnisse: Was Sie nach diesem Kapitel können sollten

Abschnitt betitelt „Lernergebnisse: Was Sie nach diesem Kapitel können sollten“

Nach Abschluss dieses Kapitels sollten Schülerinnen und Schüler in der Lage sein:

  • Nennen: die Elemente des Klassendiagramms (Klasse, Sichtbarkeit, Assoziation, Multiplizität, Aggregation, Komposition, Vererbung, abstrakte Klasse, Interface) und ihre Notation nennen.
  • Anwenden: die Struktur einer gegebenen Problemstellung analysieren und als Klassendiagramm mit Vererbung und Beziehungen darstellen.
  • Übertragen: zwischen Klassendiagramm und Python-Code in beide Richtungen übersetzen.
  • Anwenden: Diagramme textbasiert mit Mermaid oder PlantUML erstellen und versionieren.
  • Beurteilen: fremde Modelle anhand nachvollziehbarer Kriterien prüfen und begründet Verbesserungen vorschlagen.
  • Aufgabe 21 - Modellieren einer Problemstellung (Teil Statik)