UML-Klassendiagramm Objektbeziehungen - Assoziationen

https://bildung.social/@oerinformatik/110425552692026065

https://oer-informatik.de/uml-klassendiagramm-assoziation

tl/dr; (ca. 10 min Lesezeit): Mit einzelnen Objekten erreicht man in der objektorientierten Programmierung (OOP) noch nicht viel. Erst im Zusammenspiel als Objektsammlungen und über Objektinteraktionen entwickelt die OOP ihre Stärken. Der folgende Text behandelt die Fragen: Welche Objektbeziehungen gibt es? Was unterscheidet Abhängigkeit, Assoziation, Aggregation und Komposition? Versuch der Klärung. (Zuletzt geändert am 14.04.2025)

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

In der Objektorientierung können Klassen oder deren Objekte in Beziehung zueinander stehen. Klassen stehen beispielsweise bei der Vererbung miteinander in Beziehung und teilen sich so eine gemeinsame Struktur. Doch auch die einzelnen Instanzen der Klassen (Objekte) können Beziehungen zueinander haben: je nach Stärke und Art der Beziehung sprechen wir von Abhängigkeiten, Assoziationen, Aggregationen oder Kompositionen. Wir nähern uns den Objektbeziehungen schrittweise:

Abhängigkeit

Eine Abhängigkeit (Dependency) stellt in der UML die allgemeinste Form einer Beziehung zwischen zwei Elementen dar. Mit “Element” können hier Klassen oder Objekte gemeint sein. Man unterscheidet ein unabhängiges Element (Supplier) und ein von diesem abhängiges Element (Client).

Die Abhängigkeit einer Client-Klasse von einer Supplier-Klasse kann strukturell (den Aufbau betreffend) oder semantisch (die Bedeutung betreffend) begründet sein. Sie kann die Klassen selbst oder deren Objekte betreffen. Allen gemein ist, dass eine Veränderung des Suppliers Einfluss auf den Client haben kann.

Beispiel: Ein Kundenobjekt benötigt in seiner eigenen Methode bezahleRechnung(...) ein Rechnungsobjekt als Parameter, und ruft auf diesem eine Methode (setBezahlt()) auf. Kunde benötigt also die Klasse Rechnung in der Implementierung. Ändert sich die Implementierung oder Struktur von Rechnung, so hat das Einfluss auf Kunde:

Abhängigkeit zwischen Kunde und Rechnung
Abhängigkeit zwischen Kunde und Rechnung

Abhängigkeiten können unterschiedlichster Natur sein: Client-Objekte können beispielsweise Methoden der Supplier-Objekte aufrufen (<<call>>, s.o.), deren Objekte erzeugen (<<create>>) oder sie ganz allgemein nutzen (<<use>>). Abhängigkeiten gibt es auch auf Klassenebene, <<substitute>> beschreibt beispielsweise, dass ein Element ein anderes ersetzt. Die UML kennt eine Vielzahl weiterer Dependency-Arten, die alle als Stereotyp mit den doppelten spitzen Klammern (Guillemets) notiert werden. Im UML-Standard finden sich Dependencies im Kapitel 7.7..

Aufgrund der Vielzahl an Dependencies ist deren vollständige Modellierung in der Regel nicht sinnvoll. Man beschränkt sich daher häufig in UML-Diagrammen auf die Darstellung der nun folgenden Objektbeziehungen: Assoziation, Aggregation und Komposition.

Assoziation

Wenn zwei Objekte miteinander interagieren können (also ein Objekt eine Methode des anderen aufruft oder dessen Zustand beeinflusst), so nennt man diese Beziehung eine Assoziation (UML Spec 11.5). Beide Objekte arbeiten für ein gemeinsames Ziel zusammen. Die Beziehungen werden im UML Klassendiagramm durch eine Linie zwischen den Objekten dargestellt:

Unspezifische Assoziation zwischen Kunde und Rechnung: Rechnung kann Kundenattribut sein und Kunde Rechnungsattribut
Unspezifische Assoziation zwischen Kunde und Rechnung: Rechnung kann Kundenattribut sein und Kunde Rechnungsattribut

Das gemeinsame Ziel der beteiligten Objekte wird oft anhand des Assoziationsnamens klar. Phrasen wie “benutzt ein”, “ist zugeordnet zu” oder “hat eine Beziehung zu” können diese Assoziationen recht unkonkret beschreiben, häufig finden sich präzisere Namen der Beziehung. Diese Assoziationsnamen werden auf der Assoziationskante mit Pfeil in Leserichtung angegeben: ein/e Kund*in bezahlt eine Rechnung.

Assoziation mit Name und Leserichtung: ein Kunde bezahlt eine Rechnung
Assoziation mit Name und Leserichtung: ein Kunde bezahlt eine Rechnung

Assoziationen stellen eine alternative Darstellung von Attributen dar: eines der Objekte ist ein Member (Attribut) des anderen Objekts. Wenn Assoziationen als einfache Linien dargestellt werden, ist nicht eindeutig festgelegt, welches Objekt ein Attribut des anderen ist (Kunde ein Attribut von Rechnung, Rechnung ein Attribut von Kunde, oder beides). Mit einer unspezifischen Assoziation kann ich mir dieses Implementierungsdetail also bewusst offen halten.

Ich halte es bei einer unspezifischen Assoziation (also einer einfachen Linie) zwischen zwei Objekten auch für zulässig, diese Beziehung als zeitlich begrenzte Abhängigkeiten zu realisieren: Wenn die Rechnung bezahlt ist, haben Kund*in und Rechnung also ggf. keine weitere Beziehung zueinander:

Eine Kund*in bezahlt eine Rechnung: konkretere Darstellung
Eine Kund*in bezahlt eine Rechnung: konkretere Darstellung

Das Ziel der Zusammenarbeit ist das Bezahlen einer Rechnung. Dazu erhält ein Kundenobjekt bei Aufruf der Methode bezahleRechnung() ein Rechnungsobjekt übergeben. Die Kund*in nutzt die Rechnung-Methoden getRechnungsbetrag() und setBezahlt(), um die Rechnung von seinem vermoegen zu bezahlen. Nach Abschluss dieser Transaktion hält der Kunde keine Referenz mehr auf die Rechnung - beide Objekte können unabhängig voneinander fortbestehen.

In der Literatur wird die Assoziation aber häufig enger interpretiert: als eine dauerhafte Verbindung zwischen den Objekten in Form eines Attributs. Nach dieser Logik müsste man bei der oben dargestellten Beziehung mit der Parameter-Übergabe von Rechnung auf eine <<call>> oder <<use>>-Dependency zurückzugreifen, wie ich das im Beispiel der Dependencies gezeigt hatte. Das gilt dann, wenn ein Objekt nur als Parameter, Rückgabewert oder lokale Variable innerhalb des anderen verwendet wird. Ich kann diese Einschränkung aus dem Standard nicht herauslesen, bin aber - wie immer - für alle Hinweise dankbar (UML Spec 11.5: Assoziation).

Gerichtete Assoziation

Bei den bisherigen Darstellungen wurde im UML-Diagramm offengelassen, ob beide Objekte einander kennen. Beide Enden der Assoziationskante waren unspezifiziert.

Wenn genauer festgelegt werden soll, welches Objekt vom anderen weiß - also Zugriff auf eine Referenz hat - kann dies mit Kantenenden spezifisch dargestellt werden:

Unidirektional navigierbare Assoziationen

unidirektionale Navigation von Kunde zu Rechnung
unidirektionale Navigation von Kunde zu Rechnung

Ein Kunde kann auf die Rechnung zugreifen (Pfeil in Richtung Rechnung), eine Rechnung weiß jedoch nicht, welche Kund*in sie bezahlt hat (Kreuz auf der Kantenseite Kunde). Eine Rechnung hält also keine Referenz (als Attribut oder Parameter) auf Kunde. Das wird v.a. auch im Beispielcode deutlich:

Da diese Assoziation nur in einer Richtung navigiert werden kann, spricht man von einer unidirektional navigierbaren Assoziation.

Bidirektional navigierbare Assoziation

Wird beispielsweise beim Bezahlvorgang aufseiten der Rechnung vermerkt, welche Kund*in die Rechnung bezahlt hat (z.B. in der Methode setBezahlt(istBezahlt:boolean, kunde:Kunde)), so kennen sich beide Objekte und halten Referenzen (als Attribut oder Parameter) des anderen. Die Assoziation kann in beide Richtungen navigiert werden (Kunde ruft Methoden von Rechnung auf und umgekehrt). Wir sprechen dann von einer bidirektionale navigierbaren Assoziation. In der UML wird dies durch zwei Pfeilenden an der Assoziationskante gekennzeichnet:

Kunde kennt Rechnung, Rechnung kennt Kunde
Kunde kennt Rechnung, Rechnung kennt Kunde

Im Beispielcode wird der gegenseitige Methodenaufruf der beiden Klassen deutlich:

Nicht navigierbare Assoziation

Modellierbar ist auch der Fall, dass keine Richtung navigierbar ist. Keines der Objekte hält dann direkt Referenzen auf das jeweils andere (zwei “x” an den Kantenenden), es besteht nur eine logische, aber keine faktische Beziehung. Denkbar beispielsweise, dass gegenseitig lediglich die Werte eines Objekts übergeben werden (Call by Value), aber kein Zugriff auf das Objekt selbst besteht. Ich bin für jeden Hinweis dankbar, wann es einen sinnvollen Anwendungsfall hierfür gibt und wie dieser implementiert werden muss.

Multiplizitäten

Bei den bisherigen Notationsmitteln für Assoziationen wurde noch keine Festlegung getroffen, wie viele Objekte der einen Klasse mit wie vielen Objekte der anderen Klasse in Beziehung stehen. Im folgenden Diagramm sind eine Reihe von Festlegungen für ein Programm zur Erstellung von Rechnungen getroffen. Zu jeder Kante können zwei unterschiedliche Multiplizitäten ausgewiesen werden: für jeder der beiden Klassen je eine:

Rechnung, Rechnungsanschrift, Lieferanschrift, Rechnungsposition und Steuersatz
Rechnung, Rechnungsanschrift, Lieferanschrift, Rechnungsposition und Steuersatz

Sofern die Multiplizität die Zahl 1 überschreitet, werden die entsprechenden Referenzen in Objektsammlungen gespeichert. Je nach Programmiersprache können diese als Array, ArrayList, List o.ä. umgesetzt werden. Jede Multiplizität legt fest, wie viele Instanzen der Klasse, an der sie notiert ist, an der Beziehung mindestens und höchstens beteiligt sind. Möglich sind folgende Notationen:

UML-Notation
am Kantenende
Bedeutung Beispiel
n oder
n..n

(n ist positive Ganzzahl)
feste Anzahl:
Beziehung zu genau n Instanzen
Auf jeder Rechnung wird genau eine Lieferanschrift genannt; Jede Rechnungsposition ist genau einer Rechnung zugeordnet. Da hier Unter- und Obergrenze identisch sind (z.B. 3..3) kann eine Grenze weggelassen werden (3).
0..1 optional:
Beziehung zu keiner oder einer Instanz
Eine Rechnungsanschrift ist optional. Eine Rechnung kann eine gesonderte Rechnungsanschrift haben (andernfalls ist sie identisch mit der Lieferanschrift).
m..n

(n und m sind Ganzzahlen, 0 <= m < n)
Beziehungen zwischen m und n unterschiedlichen Instanzen Es gibt genau zwei Steuersätze (ermäßigt/normal). Auf jeder Rechung wird mindestens ein Steuersatz ausgewiesen, höchstens aber beide.
* oder
0..*
null oder mehr
Beziehung zu beliebig vielen Instanzen (oder zu keiner)
Ein Steuersatz kann auf beliebig vielen Rechnungen ausgewiesen sein. Denkbar auch, dass er auf keiner Rechnung ausgewiesen wird. Anstelle von 0..* kann auch verkürzt * notiert werden.
n..*

(n ist positive Ganzzahl)
mindestens n oder mehr Auf jeder Rechnung findet sich mindestens eine Rechnungsposition.

Im Programmtext muss anhand des Datentyps der Referenz oder über Implementierung sichergestellt werden, dass die jeweilige Anzahl an Instanzen übergeben werden kann.

Die Begriffe Kardinalität und Multiplizität

In der UML wird eine tatsächliche Anzahl der verknüpften Instanzen Kardinalität genannt. An folgendem Beispiel soll das deutlich werden: Ein Fahrzeug kann eines oder viele Räder haben.

Rechnung, Rechnungsanschrift, Lieferanschrift, Rechnungsposition und Steuersatz
Rechnung, Rechnungsanschrift, Lieferanschrift, Rechnungsposition und Steuersatz

Ist das Fahrzeug ein Fahrrad, so beträgt die tatsächliche Anzahl (die Kardinalität) 2, bei einem Auto 4, bei einem Einrad 1, bei einer Ape 3 und bei einem Zug ein Vielfaches von 4 - z.B. 128. Der Begriff Kardinalität beschreibt in der UML also die konkrete Anzahl an Beziehungen einer identifizierten Instanz der Klasse. Das weicht von der Bedeutung ab, mit der Kardinalität bei der Datenbankmodellierung (Entity Relationship Model) verwendet wird - dort ist Kardinalität synonym zu Multiplizität zu sehen.

In der UML ist der Begriff Multiplizität definiert als die Menge aller zulässigen Kardinalitäten. Ein Fahrzeug allgemein kann ein oder viele Räder haben: Die Multiplizität ist daher 1..*.

Aggregation

Assoziationen beschreiben eine allgemeine, einem Ziel zugeordnete Zusammenarbeit zweier Objekte. Wir können diese mit “benutzt ein” beschreiben. Eine engere Bindung zweiter Objekte ist gegeben, wenn Objekte zueinander eine dauerhafte Teil-Ganzes-Beziehung haben, die mit Phrasen wie “besteht aus” oder “hat ein/e” beschrieben werden können (in umgekehrter Leserichtung: “ist Teil von”).

Die zugeordneten Instanzen der Teile werden in Attributen des Ganzen verwaltet. Per Setter-Methoden oder im Konstruktor wird das Ganze (das Aggregat) zusammengesetzt, somit im wörtlichen Sinne aggregiert. Man nennt diese Beziehung daher Aggregation. Im UML-Standard wird sie im Kapitel 9.5.3 beschrieben (etwas versteckt auf der Folgeseite, vor einer Tabelle).

Im UML-Klassendiagramm wird diese besondere Assoziation durch eine nicht ausgefüllte Raute aufseiten des Ganzen notiert.

Ein Konto ist Teil des Objekts Kunde
Ein Konto ist Teil des Objekts Kunde

Bei einer Aggregation kann das Teil auch unabhängig vom Ganzen existieren - es gibt Referenzen auf das Teil außerhalb des Ganzen (z.B. in dem die Teil-Instanz außerhalb des Ganzen erzeugt wird).

Es kann auch Teilinstanzen geben, die mehreren Ganzen zugeordnet sind. Denkbar wäre im obigen Beispiel etwa, dass Konten mehreren Kund*innen zugeordnet werden oder eine Kund*in gelöscht wird, die Konten aber weiterbestehen. Ein Quelltextbeispiel:

Komposition

Bei der Komposition handelt es sich um eine Sonderform der Teil/Ganzes-Beziehung: Die Teilobjekte existieren nur innerhalb des Ganzen und sind somit in ihrer Lebensdauer auf die Lebensdauer des zugehörigen Ganzen beschränkt. Da dies eine festere Bindung zwischen Teil und Ganzem ist, wird im UML-Standard (Kapitel 9.5.3) auch von einer “starken Form der Aggregation” gesprochen.

Aufgrund dieser Abhängigkeit kann ein Teil auch nur mit einem Ganzen in Beziehung stehen - die Multiplizität aufseiten des Ganzen ist somit “1” (der Defaultwert) und wird daher oft weggelassen.

Die Komposition wird mir einer ausgefüllten Raute aufseiten des Ganzen gekennzeichnet:

Eine Rechnungsposition ist existenziell abhängig von einer Rechnung
Eine Rechnungsposition ist existenziell abhängig von einer Rechnung

Kompositionen erkennt man in der Programmierung in der Regel daran, dass das Teil innerhalb des Ganzen erzeugt wird und das Teilobjekt nicht über Getter oder öffentliche Attribute nach außen veröffentlicht wird. Nur das Ganze hält Referenzen auf das Teil.

Die Entscheidung, ob eine Beziehung als Aggregation oder Komposition umgesetzt wird, trifft der/die Programmierer*in. Häufig gibt die Fachdomäne über gegebene Abhängigkeiten bereits Hinweise, was sinnvoll ist. In der Regel führt die Komposition zu besser wartbarem Code, da weniger externe Abhängigkeiten möglich sind.

Quelltextbeispiel:

Für Experten / Hintergrund

Die UML lässt die Möglichkeit, dass Teile vom Ganzen entkoppelt werden und dann auch nicht mit dem Ganzen gelöscht werden. Um diesen Sonderfall abzubilden, müsste man eine Getter-Methode implementieren, die innerhalb des Ganzen die Referenz auf das Teil löscht, sobald es das Ganze verlässt. In obigem Beispiel könnte so eine Methode in Rechnung etwa so aussehen:

Übersicht von Assoziation, Aggregation und Komposition

Die unterschiedlichen Objektbeziehungen anhand der oben genannten Beispiele ergeben insgesamt die Modellierung eines Abrechnungssystems:

Die unterschiedlichen Objektbeziehungen zwischen Konto, Kunde, Rechnung und Rechnungsposition als UML-Klassendiagramm
Die unterschiedlichen Objektbeziehungen zwischen Konto, Kunde, Rechnung und Rechnungsposition als UML-Klassendiagramm

Das ganze Beispiel als Java-Code

Wer versucht hat, das ganze per UML geplante OOP-Programm in Java nachzuvollziehen, sollte etwa diesen Quelltext am Ende vor sich haben und ausführen können:

Alle Klassen aus den vergangenen UML-Modellierungsbeispielen spannen folgende Implementierung auf:

Starterklasse Investmentrechner in Datei Investmentrechner.java:

Die Konto-Klasse und ihre Spezialisierungen in der Datei Konto.java

package de.oerinformatik.crm;

import java.util.ArrayList;

public class Konto{
    private String iban;
    protected double kontostand =0.0;
    static ArrayList<Konto> kontenListe = new ArrayList<>();
    
    public String getIban() {
        return iban;
      }
    
    public void setIban(String iban) {
        this.iban = iban;
    }

    void einzahlen(double betrag) {
      kontostand = kontostand + betrag;
    }

    boolean auszahlen(double betrag) {
        if (kontostand - betrag > 0){
            kontostand = kontostand - betrag;
            return true;
        }else{
            return false;
        }
    }
    
    static String listeStatusAllerKonten(){
        String ausgabe ="";
        for (Konto einKonto: kontenListe){
            ausgabe = ausgabe + "\n" + einKonto.kontostatus();
        }
        return ausgabe;
    }
    
    String kontostatus(){
        return "KontoNr "+this.iban+" Kontostand: "+this.kontostand.toString();
    }

    Konto(String iban){
        this.iban = iban;
        kontenListe.add(this);
    }

    Konto(String ktn, String blz){
        this.iban = "DE__"+blz+ktn;
        kontenListe.add(this);
    }

    public double getKontostand() {
        return kontostand;
    }
  }

  class PrivatKonto extends Konto{
    private String vorname;
    private String nachname;

    public PrivatKonto(String iban, String vorname, String nachname){
        super(iban);
        this.vorname = vorname;
        this.nachname = nachname;
    }
    public String getPrivatkundenName(){
      return this.vorname + " " + this.nachname;
    }   
}

class GeschaeftsKonto extends Konto {

    private String firmenname;

    private double verfuegungsrahmen;

    public double getVerfuegungsrahmen() {
        return verfuegungsrahmen;
    }

    public void setVerfuegungsrahmen(double verfuegungsrahmen) {
        this.verfuegungsrahmen = verfuegungsrahmen;
    }

    public GeschaeftsKonto(String iban, String firmenname, double verfuegungsrahmen) {
        super(iban);
        this.firmenname = firmenname;
        this.setVerfuegungsrahmen(verfuegungsrahmen);
    }

    @Override
    public boolean auszahlen(double betrag) {
      if (betrag > verfuegungsrahmen) {
          return false;
      } else {
          return super.auszahlen(betrag);
      }  
    }  
  }

class SparKonto extends PrivatKonto{
    private double zinssatz;
    public double getZinssatz() {return zinssatz;}
    public void setZinssatz(double zinssatz) {this.zinssatz = zinssatz;}

    public SparKonto(String iban , String vorname, String nachname, double startzins ){
        super(iban, vorname, nachname);
        this.setZinssatz(startzins);
    }

    public void verzinse(){
        this.kontostand = this.kontostand * (1+zinssatz);
    }  
}

Die Klasse Kunde in der Datei Kunde.java:

Die Klassen Rechnung und Rechnungsposition in der Datei Rechnung.java:

package de.oerinformatik.crm;

import java.util.ArrayList;

public class Rechnung {
    private double rechnungsbetrag;
    private boolean bezahlt;
    private String bezahltVon = "";

    public double getRechnungsbetrag(){
        return rechnungsbetrag;
    }

    public void setRechnungsbetrag(double rechnungsbetrag){
        this.rechnungsbetrag = rechnungsbetrag;
    }

    public void setBezahlt(boolean bezahlt, Kunde kunde) {
        this.bezahlt = bezahlt;
        bezahltVon = kunde.toString();
        System.out.println("wurde gezahlt von "+bezahltVon);
    } 

   private ArrayList<Rechnungsposition> rechnungspositionen = new ArrayList<Rechnungsposition>();

   public void addRechnungsposition(String name, String artikelnummer, Integer anzahl, double nettopreis) {
       rechnungspositionen.add(new Rechnungsposition(name, artikelnummer, anzahl, nettopreis));
   }

   public Rechnungsposition removeRechnungsposition(int index) {
    return rechnungspositionen.remove(index);
    }

   @Override
   public String toString() {
       String text = "Rechnung: \n";
       for (Rechnungsposition position : rechnungspositionen) {
           text += rechnungspositionen.toString() + "\n";
       }
       if (this.bezahlt){
        text += "Rechnung wurde gezahlt von "+bezahltVon+ "\n";
       }else{
        text += "Rechnung ist noch offen"+ "\n";
       }
       return text;
    } 
}

class Rechnungsposition {

    private String name;
    private String artikelnummer;
    private Integer anzahl;
    private double nettopreis;

    Rechnungsposition(String name, String artikelnummer, Integer anzahl, double nettopreis) {        this.name = name;
        this.artikelnummer = artikelnummer;
        this.anzahl = anzahl;
        this.nettopreis = nettopreis;
    }
 
    @Override
    public String toString() {
        return "Name: " + this.name + " /  Artikelnummer: " + artikelnummer + " / Anzahl: " + this.anzahl + " / Nettoeinzelpreis: " + this.nettopreis;
    }    
}

Nächster Schritt: Klassenbeziehungen

Wir haben jetzt die Beziehungen behandelt, die konkrete Instanzen (also Objekte) untereinander haben können. Im folgenden Schritt soll es darum gehen, dass wir Verhalten und Strukturen, die in Klassen definiert sind, an andere Klassen vererben können: es geht also um Klassenbeziehungen (Vererbung).


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: UML-Klassendiagramm Objektbeziehungen - Assoziationen” von oer-informatik.de (H. Stein), Lizenz: CC BY 4.0. Der Artikel wurde unter https://oer-informatik.de/uml-klassendiagramm-assoziation veröffentlicht, die Quelltexte sind in weiterverarbeitbarer Form verfügbar im Repository unter https://gitlab.com/oer-informatik/uml/umlklasse. Stand: 14.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: