Objekt-orientiertes Design in C++: Datenkapselung und UML-Diagramme

Mit den drei Zugriffs-Spezifikationen (access modifier) public, protected und private wird festgelegt, welche Klasse auf welche Datenelemente und Methoden zugreifen kann. Mit ihrer Hilfe lĂ€sst sich die Datenkapselung realisieren. Die Zugriffs-Spezifikation ist ein Bestandteil der Deklaration von Datenelementen und Methoden; es wird gezeigt, wie man die Deklarationen einer Klasse ĂŒbersichtlich in einem UML-Diagramm darstellen kann. UML-Diagramme sind somit eines der wichtigsten Hilfsmittel beim Entwurf komplexer Programme.
Noch keine Stimmen abgegeben
Noch keine Kommentare

Einordnung des Artikels

In den bisherigen Kapiteln ĂŒber C++: Fortgeschrittene Syntax wurde ein kleines Statistik-Projekt entwickelt. An diesem wird nun gezeigt:

  • Wie ein Projekt erweitert werden kann (ohne große VerĂ€nderungen an den bestehenden Quelltexte vorzunehmen).
  • Wie man mit den Zugriffs-Spezifikationen (access modifier) public und private Daten kapseln kann.
  • Insbesondere wird gezeigt, welche Zufallsgeneratoren die Standard-Bibliothek bereitstelllt und wie man einen Zufallsgenerator in das Projekt einbauen kann (wiederum, ohne große VerĂ€nderungen am Quelltext vorzunehmen).
  • Wie man die Deklarationen einer Klasse ĂŒbersichtlich in UML-Diagrammen darstellt; sie werden ĂŒblicherweise beim Entwurf von Klassen eingesetzt.

Datenkapselung

Die Zugriffs-Spezifikation (access modifier)

Bei der Definition einer Klasse wurde bereits gesagt, dass die Datenelemente und Methoden entweder public oder private bezeichnet werden. Eine Erweiterung des Statistik-Projektes kann die Bedeutung dieser Zugriffs-Spezifikation (im Englischen werden public und private als access modifier bezeichnet) besser verstÀndlich machen. In den Aufgaben zur strukturierten Programmierung wurden schon Erweiterungen (wie Berechnung der Varianz, Standardabweichung, Kovarianz, Korrelationskoeffizient) vorgeschlagen.

Fragt man sich, wie man diese Erweiterungen realisieren soll, fallen sofort zwei Punkte auf:

  1. Bisher gab es nur eine relevante statistische Funktion, nĂ€mlich der Mittelwert einer Zahlenfolge. Und diese Funktion war als statische Funktion realisiert — sie gehört also nicht zu einem Objekt sondern zur Klasse Statistics. Die neuen Funktionen werden dann sicher auch als statische Funktionen implementiert.
  2. Zur Berechnung der Varianz und der Kovarianz ist es hilfreich, eine weitere statische Funktion anzubieten, nÀmlich das Skalarprodukt zweier Zahlenfolgen:

(x1, x2, ... , xn) ‱ (y1, y2, ... , yn) =∑i xi yi

Der erste Punkt spricht dafĂŒr, das Skalarprodukt und die statistischen Funktionen gleichberechtigt zu behandeln. Dagegen spricht der zweite Punkt fĂŒr eine Unterscheidung zwischen dem Skalarprodukt und den anderen statistischen Funktionen. Denn das Skalarprodukt ist eine reine Hilfsfunktion, die dem Programmierer die Arbeit erleichtert, wenn er die anderen statistischen Funktionen implementieren muss. Im Gegenatz dazu werden die statistischen Funktionen vom Nutzer des Programmes aufgerufen (im bisher entwickelten Projekt war dies der Client). Das Skalarprodukt sollte fĂŒr den Nutzer nicht sichtbar sein, da es eigentlich nicht zu den statistischen Funktionen gehört — und hier kein Geometrie-Programm entwickelt werden soll.

Aber dieser Unterschied erklÀrt gerade die Verwendung der access modifier public und private:

  1. Datenelemente und Methoden sind public, wenn sie nach außen sichtbar sein sollen: andere Klassen können auf ihre Informationen zugreifen und ihre FunktionalitĂ€ten nutzen.
  2. Datenelemente und Methoden sind private, wenn sie nicht nach außen sondern nur innerhalb der eigenen Klasse sichtbar sein sollen.

Indem man fĂŒr jedes Datenelement und jede Methode eine Zugriffs-Spezifikation festlegt, kann man bestimmte Daten (also Datenelemente und auch Algorithmen, die darauf zugreifen) in einer Klasse kapseln. Die Vorteile dieser Datenkapselung liegen auf der Hand:

  1. Die öffentlichen Methoden werden ĂŒblicherweise schon beim Entwurf eines Programmes festgelegt und sollten sich danach nicht mehr Ă€ndern; es sollen höchstens Erweiterungen hinzukommen, die sich aber bei einem geschickten Entwurf leicht verwirklichen lassen.
  2. Da man beim Entwurf bereits alle Teile des Programmes und ihre Verantwortlichkeiten definiert, kann man in dieser Phase rein auf der Ebene der Deklarationen von öffentlichen Datenelementen und Methoden arbeiten.
  3. Zum Zeitpunkt des Entwurfes muss man noch nicht exakt wissen, wie spĂ€ter die TĂ€tigkeiten der Objekte als Algorithmen implementiert werden. Da diese aber nach außen nicht sichtbar sind, können diese privaten Teile einer Klasse weitgehend unabhĂ€ngig von den öffentlichen Schnittstellen der Klasse entwickelt werden (weitgehend unabhĂ€ngig, da sie natĂŒrlich die Deklarationen der öffentlichen Datenelemente und Methoden beachten mĂŒssen).
  4. Und sollte sich im Lauf der Entwicklung des Programmes herausstellen, dass einige dieser privaten Teile anders (besser, schneller, effektiver) realisiert werden können, ist es nicht schwer diese Teile auszutauschen; die öffentlichen Schnittstellen sollten davon nicht betroffen sein.

Anwendung auf das Statistik-Projekt

Mit diesem Hintergrundwissen kann man leicht die Deklarationen der Klasse Statistics angeben (so dass die oben beschriebenen Erweiterungen enthalten sind); achten Sie insbesondere auf die access modifier:

// Statistics.h
// enthÀlt die Deklarationen der Klasse Statistics

#ifndef STATISTICS_H_INCLUDED
#define STATISTICS_H_INCLUDED

#include<vector>

class Statistics
{
public:

// Berechnet den Mittelwert eines Vektors (Zufallsfolge randomSequence = rs)
static double mean(std::vector<int> rs);

// Berechnet die Varianz eines Vektors (Zufallsfolge randomSequence = rs)
static double variance(std::vector<int> rs);

// Berechnet die Standardabweichung eines Vektors (Zufallsfolge randomSequence = rs)
static double standardDeviation(std::vector<int> rs);

// Berechnet die Kovarianz zweier Vektoren
static double covariance(std::vector<int> a, std::vector<int> b);

// Berechnet den Korrelationskoeffizient zweier Vektoren
static double correlation(std::vector<int> a, std::vector<int> b);

// =================================
//     Regressionsgerade y = ax + b
// =================================

// Berechnet die Steigung a der Regressionsgerade
static double slope(std::vector<int> a, std::vector<int> b);

// Berechnet den y-Abschnitt (y-intercept) b der Regressionsgerade
static double getYIntercept(std::vector<int> a, std::vector<int> b);

private:

// Berechnet das Skalarprodukt zweier Vektoren
// FĂŒr den Fall unterschiedlicher LĂ€nge: Abbruch der Summation beim Minimum der LĂ€ngen
static double scalarProduct(std::vector<int> a, std::vector<int> b);
};

#endif // STATISTICS_H_INCLUDED

Aufgaben:

1. Hier verwenden alle Methoden call by value. Diskutieren Sie: Welcher Übergabe-Mechanismus ist vorzuziehen call by value oder call by reference?

2. Erweitern Sie Ihr Statistik-Projekt um obige Deklarationen und verwenden Sie den geeigneten Übergabe-Mechanismus!

3. Implementieren Sie die noch fehlenden Methoden; wenn Sie die Methoden in einer geschickten Reihenfolge implementieren, ĂŒbernimmt die private Methode scalarProduct() nahezu die gesamte Arbeit.

Einbau eines Zufallsgenerators

So wie die Klasse RandomGenerator bisher implementiert wurde, hĂ€lt sie nicht was sie verspricht: Die Methode createRandomSequence() erzeugt zwar eine Zahlenfolge (mit dem gewĂŒnschten Wertebereich zwischen min und max), es ist aber noch keine Zufallsfolge.

Sie sollten auch nicht versuchen, einen Zufallsgenerator selber zu implementieren: die Aufgabe ist deutlich schwerer als sie erscheint.

Aber es gibt in der Standard-Bibliothek zwei Zufallsgeneratoren zur Auswahl, die man in die Klasse RandomGenerator einbauen kann.

Die Zufallsgeneratoren aus der Standard-Bibliothek

Zufallszahlen, die mit Hilfe des Computers erzeugt werden, entspringen immer Algorithmen und sind somit nur Pseudo-Zufallszahlen. Das heißt sie sind eigentlich periodische Zahlenfolgen; allerdings ist die PeriodenlĂ€nge derart groß, dass man sie bei gewöhnlichen Anwendungen nicht erkennt und die Zahlen doch wie Zufallsfolgen erscheinen.

Selbst die Implementierung eines Algorithmus, der Pseudo-Zufallszahlen erzeugt, ist sehr schwer; insbesondere kann es leicht passieren, dass man an den produzierten Folgen — selbst weit unterhalb der PeriodenlĂ€nge — RegelmĂ€ĂŸigkeiten ablesen kann, etwa dass es bevorzugte Frequenzen gibt. Das soll heißen, die Zufallsfolge beinhaltet zwar alle Zahlen xmin, ... xmax mit gleicher Wahrscheinlichkeit; bei nĂ€herer Betrachtung stellt man aber fest, dass es eine Zahl n gibt, so dass eine Zahl x der Folge nach n, 2n, 3n, ... Wiederholungen des Zufallsexperimentes mit deutlich erhöhter Wahrscheinlichkeit auftritt.

Es gibt zwei Möglichkeiten, wie man mit Hilfe der Standard-Bibliothek Zufallszahlen erzeugen kann; und diese Zufallsgeneratoren unterscheiden sich in ihrer QualitĂ€t, wobei es fĂŒr einfache Anwendungen auf die Unterschiede nicht ankommen sollte:

1. Die Àltere Version, die man mit

#include <cstdlib>

inkludiert; sie besitzt insbesondere den Funktionen rand() und srand(). FĂŒr ernsthafte Anwendungen (wie zur VerschlĂŒsselung) wird sie ausdrĂŒcklich nicht empfohlen. FĂŒr eine Anwendung wie hier — um Testdaten zu erzeugen — reicht sie allemal.

Die anspruchsvolleren, neuen Zufallsgeneratoren (seit dem Standard C++11), die man in der Standard-Bibliothek unter Numerics library, Pseudo-random number generation findet. Sie bieten mehrere Generatoren unterschiedlicher QualitÀt, die man zusÀtzlich mit unterschiedlichen Verteilungen konfigurieren kann.

Der Zufallsgenerator aus cstdlib

Der Zufallsgenerator aus cstdlib besteht im Wesentlichen aus zwei Funktionen, die nach dem Inkludieren von cstdlib wie globale Funktionen eingesetzt werden können:

  1. Mit Hilfe von srand() wird der Zufallsgenerator initialisiert (s steht fĂŒr seed).
  2. Mit rand() wird eine Zufallszahl (vom Datentyp int) zwischen 0 und RAND_MAX erzeugt; den Zahlenwert von RAND_MAX sollten Sie vor dem Gebrauch bestimmen.

Bei der Verwendung dieser Funktionen ist zu beachten:

  • Ohne Initialisierung wird der Zufallsgenerator automatisch mit srand(1) initialisiert.
  • Wird der Zufallsgenerator immer mit derselben Zahl n initialisiert, so wird auch immer wieder dieselbe Zufallsfolge erzeugt.
  • Um Letzteres zu vermeiden, kann man den Zufallsgenerator vor jedem neuen Gebrauch mit der aktuellen Uhrzeit oder der Zeitspanne seit dem Programmstart initialisieren (aus ctime, siehe Beispiel unten).
  • Die Konstante RAND_MAX ist maschinenabhĂ€ngig.
  • Die Zufallszahlen zwischen 0 und RAND_MAX sind gleichverteilt.
  • Um Zufallszahlen in einem gewĂŒnschten Bereich zu erzeugen, muss man die von rand() gelieferten Zahlen geeignet transformieren (siehe Beispiel unten).

In der Klasse RandomGenerator wird dazu die Methode createRandomSequence() abgeÀndert:

// FĂŒr den Einbau des Zufallsgenerators relevante Teile aus RandomGenerator.cpp

#include <ctime>
#include<cstdlib>

// Erzeugt eine Zufallsfolge mit int-Zahlen von min bis max der LĂ€nge number
vector<int> RandomGenerator::createRandomSequence(int number)
{
    int diff = max - min + 1;
    vector<int> randomSequence(number);

    srand(clock());             // Initialisierung des Zufallsgenerators
    for (int i = 0; i < number; i++)
    {
        randomSequence[i] = (rand() % diff) + min;       // Anpassen der Zufallszahl an den Zahlenbereich
        //cout << "in createRandomSequence: " << randomSequence[i] << endl;       // nur zum Testen
    }

    return randomSequence;
}

Zur ErklÀrung:

1. Zeile 3: Um auf die Funktion clock() zuzugreifen, muss ctime inkludiert werden. Deren ErklÀrung findet sich in:

C reference -> Date and time utilities

2. Zeile 4: In cstdlib befinden sich die Funktionen rand() und srand().

3. Zeile 12: Initialisierung des Zufallsgenerators mit Hilfe von clock().

4. Neu im Vergleich zur VorgĂ€ngerversion der Methode createRandomSequence() ist lediglich der Aufruf des Zufallsgenerators in Zeile 15. Nachdem der Zufallsgenerator einmal initialisiert wurde, liefert die Funktion rand() die nĂ€chste Zahl der Folge. Die Anpassung an den gewĂŒnschten Zahlenbereich ist wie in der VorgĂ€ngerversion.

5. Der Aufruf der Methode createRandomSequence() hat sich im Vergleich zur VorgÀngerversion ebenfalls nicht geÀndert.

Die Vorteile dieser Lösung sind:

  1. Da man nur globale Funktionen aufruft, kann man ganz leicht einen Zufallsgenerator einbauen: Beachten Sie wie klein die Unterschiede zur VorgÀngerversion von createRandomSequence() sind!
  2. Da die Funktion clock() bei jedem Programm-Aufruf schon eine Zufallszahl erzeugt, wird der eigentliche Zufallsgenerator jedesmal neu initialisiert und liefert bei aufeinanderfolgenden Programm-Aufrufen (mit hoher Wahrscheinlichkeit) andere Zufallsfolgen.

Der Nachteil muss aber auch genannt werden:

Die öffentliche Methode createRandomSequence() besitzt jetzt zwei Aufgaben:

  • Konfiguration des Zufallsgenerators und
  • Erzeugen der Zufallsfolge.

Und wenn man das Statistik-Projekt erweitern möchte — etwa wenn Zufallsfolgen ohne Laplace-Annahme erzeugen möchte — besteht die Gefahr, dass die Konfiguration des Zufallsgenerators mit einer selbstdefinierten Verteilungsfunktion (anstelle der Laplace-Annahme) ebenfalls in der Methode createRandomSequence() erfolgt.

Verbesserte Version der Methode createRandomSequence()

Betrachtet man nochmal genau, welche Aufgaben die Methode createRandomSequence() in der aktuellen Version oben erfĂŒllt, so muss man unterscheiden:

  1. Es wird ein Objekt eines Vektors erzeugt (nötig, um den richtigen RĂŒckgabetyp bereitzustellen).
  2. Der Zufallsgenerator wird initialisiert.
  3. Der Zufallsgenerator wird aufgerufen.
  4. In der Schleife wird die Zufallsfolge mit der gewĂŒnschten LĂ€nge erzeugt; mit Hilfe der beiden Zahlen min und max wird dafĂŒr gesorgt, dass die Elemente der Zufallsfolge den richtigen Wertebereich besitzen.

Angesichts dieser FĂŒlle von Aufgaben, ist es naheliegend, eine weitere private Methode

private:

	int nextRandom();

anzubieten, die alle Aufgaben ĂŒbernimmt, die den Zufallsgenerator betreffen. Allerdings ist es nicht nötig, bei jedem Aufruf von nextRandom() den Zufallsgenerator zu initialisieren; dies kann einmal geschehen und somit am Besten im Konstruktor.

In der Methode createRandomSequence() wird dann nur noch in der Schleife nextRandom() anstelle von rand() aufgerufen. Auch das Anpassen an die Zahlen min und max kann bereits in nextRandom() erfolgen.

Die relevanten Teile von RandomGenerator.h und RandomGenerator.cpp lauten jetzt:

// FĂŒr den Einbau des Zufallsgenerators relevante Teile aus RandomGenerator.h
// Deklaration der Klasse RandomGenerator, die Zufallsfolgen gewĂŒnschter LĂ€nge erzeugen kann
// Ein Objekt spezifiziert den Wertebereich der Zahlenfolge (siehe Konstruktor)

#ifndef RANDOMGENERATOR_H_INCLUDED
#define RANDOMGENERATOR_H_INCLUDED

#include<vector>
#include<cstdlib>
#include<random>

class RandomGenerator
{
public:

    //=========================================
    //          Konstruktoren und Destruktor
    //=========================================

    // Konstruktor zum Setzen von min und max und zusĂ€tzlicher ÜberprĂŒfung der Eingabe sowie Initialisierung des Zufallsgenerators
    RandomGenerator::RandomGenerator(int minimum, int maximum, bool validate);
    
    // ... (weitere Konstruktoren)
    
    //===========================
    //          Methoden
    //===========================

    // Erzeugt Zufallsfolge mit int-Zahlen von min bis max der LĂ€nge number
    std::vector<int> createRandomSequence(int number);
    
    // ... (weitere Methoden)
    
private:

    int min;
    int max;    
    
    // Erzeugt die nÀchste Zufallszahl im richtigen Zahlenbereich
    int nextRandom();
    
};

#endif // RANDOMGENERATOR_H_INCLUDED
//  FĂŒr den Einbau des Zufallsgenerators relevante Teile aus RandomGenerator.cpp

    //=========================================
    //          Konstruktoren und Destruktor
    //=========================================

// Konstruktor zum Setzen von min und max und zusĂ€tzlicher ÜberprĂŒfung der Eingabe sowie Initialisierung des Zufallsgenerators
RandomGenerator::RandomGenerator(int minimum, int maximum, bool validate)
{
    if (!validate)          // keine ÜberprĂŒfung von minimum und maximum
    {
        init(minimum, maximum);
    }
    else                // ÜberprĂŒfung von minimum und maximum
    {
        cout << "ÜberprĂŒfung" << endl;
        if (minimum < maximum)
        {
            init(minimum, maximum);
        }
        else
            if (minimum == maximum)
            {
                cout << "Achtung: minimum == maximum!" << endl;
                init(minimum, maximum);
            }
            else
            {
                cout << "Achtung: Sie haben minimum > maximum eingegeben!\nDie Zahlen werden richtig sortiert!" << endl;
                init(maximum, minimum);          // Achtung: Vertauschung der Reihenfolge
            }
    }
    int time = clock();
    srand(time);            // Initialisierung des Zufallsgenerators
}

// ... (weitere Konstruktoren)

    //===========================
    //          Methoden
    //===========================

// Erzeugt eine Zufallsfolge mit int-Zahlen von min bis max der LĂ€nge number
// und greift dabei auf die private Methode nextRandom() zurĂŒck
vector<int> RandomGenerator::createRandomSequence(int number)
{
    vector<int> randomSequence(number);

    for (int i = 0; i < number; i++)
    {
        randomSequence[i] = nextRandom();
        //cout << "in createRandomSequence: " << randomSequence[i] << endl;       // nur zum Testen
    }

    return randomSequence;
}

// Erzeugt die nÀchste Zufallszahl im richtigen Zahlenbereich
int RandomGenerator::nextRandom()
{
    int result;
    int diff = max - min + 1;
    result = (rand() % diff) + min;
    return result;
}

// ... (weitere Methoden)

Aufgaben

1. Erweitern Sie Ihr Statistik-Projekt um den Zufallsgenerator, der jetzt in der Methode nextRandom() enthalten ist.

2. Achten Sie dabei darauf, dass Sie das Delegations-Prinzip fĂŒr die Konstruktoren richtig einsetzen: Wenn Sie es falsch einsetzen, kann es leicht passieren, dass es einen Konstruktor gibt, der den Zufallsgenerator nicht initialisiert.

3. Erweitern Sie Ihr Projekt, so dass auch Zufallsfolgen fĂŒr einen gezinkten WĂŒrfel erzeugt werden können.

Hinweis: Überlegen Sie sich zuerst, wie der Zufallsgenerator konfiguriert werden muss, um einen WĂŒrfel mit folgenden Wahrscheinlichkeiten zu simulieren:

P(1) = 1/12, P(2) = 1/6, P(3) = 1/6, , P(4) = 1/6, P(5) = 1/6, P(6) = 3/12.

Wie lĂ€sst sich dann ein beliebiger WĂŒrfel simulieren?

4. Informieren Sie sich, welche Zufallsgeneratoren in der Numerics library (C++11) angeboten werden. Implementieren Sie die Methode nextRandom() so, dass einer dieser Zufallsgeneratoren verwendet wird. (Man sieht hier: FĂŒr eine konsequente Umsetzung der Trennung der Aufgaben, sollte auch das Initialisieren des Zufallsgenerators in eine eigene private Methode ausgelagert werden, da sich beim Einabu eines neuen Zufallsgenerators dort Änderungen vorgenommen werden mĂŒssen.)

UML-Diagramme

EinfĂŒhrung

Die letzten beiden Abschnitte sollten nochmals verdeutlichen, wie wichtig es beim Entwurf einer Klasse ist (und erst recht bei einem Projekt, das aus vielen Klassen besteht), Entscheidungen auf einer abstrakten Ebene zu treffen: es kommt vorerst weniger auf die konkrete Realisierung des Quelltextes an, sondern auf die Aufteilung der Aufgaben und die entsprechenden Deklarationen fĂŒr die Datenelemente und Methoden. Um dies zu erleichtern, gibt es als Hilfsmittel die sogenannten UML-Klassendiagramme, die alle — auf dieser Ebene — relevanten Informationen ĂŒber eine Klasse (oder ĂŒber ein ganzes Projekt) kompakt darstellen.

Die AbkĂŒrzung UML steht fĂŒr Unified Modeling Language; in dieser Sprache werden Diagramme nach einer gewissen Syntax fĂŒr Modelle erstellt. Es gibt mehrere Typen von UML-Diagrammen, wie Anwendungsfall-Diagramm (use-case), AktivitĂ€tsdiagramm, Zustandsdiagramm und so weiter. FĂŒr den objektorientierten Entwurf von Anwendungen ist mit Abstand das Klassendiagramm der wichtigste Diagrammtyp.

In einem Klassendiagramm werden die Datenelemente und Methoden einer Klasse dargestellt, siehe Abbildung 1.

Abbildung 1: UML-Klassendiagramm, das in drei Abschnitte unterteilt ist &mdash; Klassenname, Datenelemente und Methoden.Abbildung 1: UML-Klassendiagramm, das in drei Abschnitte unterteilt ist — Klassenname, Datenelemente und Methoden.

UML-Klassendiagramm fĂŒr RandomGenerator

Im UML-Klassendiagramm werden nicht nur die Datenelemente und Methoden einer Klasse dargestellt, es werden zusÀtzlich gezeigt:

  • die Konstruktoren werden wie Methoden dargestellt (der Konstruktor ist leicht als solcher zu erkennen, da hier der Funktions-Name mit dem Namen der Klasse ĂŒbereinstimmt)
  • die Datentypen der Datenelemente
  • die Datentypen der Eingabewerte von Methoden
  • die Datentypen der RĂŒckgabewerte der Methoden (der Datentyp void kann weggelassen werden)
  • die Zugriffs-Spezifikation (access modifier) der Datenelemente und Methoden; dabei steht ein Pluszeichen fĂŒr public und ein Minuszeichen fĂŒr private.

Abbildung 2 zeigt das UML-Klassendiagramm fĂŒr RandomGenerator.

Abbildung 2: UML-Klassendiagramm fĂŒr RandomGenerator (ErklĂ€rung im Text).Abbildung 2: UML-Klassendiagramm fĂŒr RandomGenerator (ErklĂ€rung im Text).

Das Klassendiagramm enthĂ€lt dieselbe Information, die in den Deklarationen der Klasse RandomGenerator enthalten ist; allerdings gibt es keine Kommentare — aber sie erĂŒbrigen sich, wenn man fĂŒr die Datenelemente, Methoden und deren Eingabewerte treffende Namen wĂ€hlt. Zu erkennen sind in Abbildung 2:

  1. Als Datenelemente gibt es nur die beiden privaten Attribute min und max.
  2. Die Methoden enthalten die Konstruktoren:
    • Default-Konstruktor
    • Konstruktor mit zwei Eingabewerten
    • Konstruktor mit drei Eingabewerten (das Delegations-Prinzip ist nicht erkennbar)
    • Kopier-Konstruktor
    • Destruktor.
  3. Als weitere Methoden:
    • die beiden öffentlichen Methoden createRandomSequence() und printSequence()
    • die öffentlichen Setter und Getter fĂŒr min und max
    • die privaten Methoden init() (ĂŒbernimmt die Aufgabe des Konstruktors mit zwei Eingabewerten bei der Realisierung der Delegation) und nextRandom() (fĂŒr den Zufallsgenerator)
    • wie oben besprochen, wĂ€re es noch sinnvoll eine private Methode anzubieten, die die Initialisierung des Zufallsgenerators ĂŒbernimmt.