Home > Informatik > Einführung in die OOP > 3. Kontrollstrukturen > 3.1 Klasse Date

3.1 Die Klasse Date

Zielsetzung

Als zweites kleineres Java-Projekt nach dem Projekt "Waage" wollen wir uns eine Anwendung vornehmen, mit der wir ausrechnen können, wie viele Tage ein Mensch genau alt ist. Wir geben dazu unser Geburtsdatum in die Anwendung ein und erhalten als Ergebnis die genaue Anzahl der Tage, die seitdem vergangen sind.

Natürlich kann man die Anwendung dann auch für andere Recherchen benutzen, zum Beispiel wie viele Tage sind seit dem Ende des 2. Weltkriegs vergangen, oder wie viele Tage ist die ältere Schwester älter als man selbst etc.

Natürlich gibt es in den zahlreichen Java-Klassen, die von den vielen Frameworks zur Verfügung gestellt werden, auch Methoden, die genau das Gleiche machen. In dieser Folge wollen wir aber lernen, wie man selbst solche Methoden entwickelt.

3.1.1 Zwei Strategien der Software-Entwicklung

Es gibt bei der Entwicklung von Anwendungen mehrere Strategien. Die zwei bekanntesten sind die Top-Down-Strategie und die Bottom-Up-Strategie. Beide Strategien haben Vor- und Nachteile, auf die ich hier nicht näher eingehen möchte. Ich persönlich bevorzuge die Bottom-Up-Strategie, vor allem bei kleineren Projekten wie dieser Datumsberechnung. Daher werde ich auch nur auf diese Strategie näher eingehen.

Die Bottom-Up-Strategie

Bei der Bottom-Up-Strategie beginnt die Entwicklung der Anwendung mit den kleinsten, grundlegendsten Komponenten oder Modulen - in Java also mit Klassen und ihren Methoden. Diese Module werden implementiert, getestet und dann schrittweise zu größeren, komplexeren Einheiten zusammengesetzt, bis schließlich das gesamte System entsteht. Man baut also von unten nach oben auf - daher auch die Bezeichnung Bottom-Up.

Vorteile

Der Vorteil dieser Vorgehensweise ist u.a. die Wiederverwendbarkeit des erzeugten Codes. Eine kleine Klasse, die man für ein Projekt A entwickelt hat, kann man bestimmt auch noch für das eine oder andere Projekt B, C, ... verwenden, man muss nicht jedes Mal das Rad neu erfinden. Das gilt vor allem dann, wenn man die Grundprinzipien der OOP berücksichtigt hat, also beispielsweise dafür sorgt, dass die entwickelte Klasse erweiterbar ist.

Ein weiterer Vorteil ist, dass man die so entwickelten Module (also Klassen und Methoden) relativ unabhängig voneinander testen kann. Auch die Testbarkeit von Modulen ist ein wichtiges Grundprinzip der OOP.

Ein dritter Vorteil ist vor allem für solche Kurse wie diesen hier interessant. Man konzentriert sich auf die Entwicklung kleiner Module, die sich leicht implementieren lassen und die Programmieranfänger nicht überfordern.

Nachteile

Es gibt natürlich auch Nachteile dieser Strategie. Der Hauptnachteil ist, dass man leicht den Blick auf das Große und Ganze verliert und sich als Entwickler in Einzelheiten der Modul-Implementierung verliert. Auch besteht die Gefahr, dass wichtige Anforderungen übersehen werden, und ohne Blick für das übergeordnete Konzept kann die Architektur der einzelnen Module uneinheitlich wirken.

3.1.2 Die Methode getDateString()

Beginnen wir mit dem ersten Modul unserer Anwendung, einer Klasse Date. Die Objekte dieser Klasse sollen ein Datum der Form TT.MM.JJJJ nicht einfach nur speichern können, sondern auch die Korrektheit dieses Datums überprüfen können. Daten wie der 31.02.2025 würden dann nicht akzeptiert werden, weil der Februar in einem Nicht-Schaltjahr nur 28 Tage hat. Ein Datum wie der 13.13.2013 würde natürlich auch nicht akzeptiert werden, da es nur 12 Monate im Jahr gibt.

Version 1:
Instanzvariablen, Konstruktor, Setter-Methoden

Die erste Version der Klasse Datum

Dieses Bild zeigt eine erste Version der Klasse Date. Die Objekte dieser Klasse besitzen drei Instanzvariablen, die die Attribute Tag, Monat und Jahr repräsentieren: day, month und year, alle drei vom Typ int. Bei der Erzeugung eines neuen Date-Objektes werden die drei Attributwerte in Form von int-Parametern übergeben. Die Zuweisung der Parameter-Werte an die Instanzvariablen wird dann aber nicht vom Konstruktor selbst übernommen, sondern an die drei Setter-Methoden delegiert.

Version 2

Diese drei Setter-Methoden liegen in der Version 1 aber erst in einer Art "Rohfassung" vor. Ihre Aufgabe wäre es nun, die Setter-Methoden so zu ändern, dass sie eine erste Überprüfung der Daten vornehmen und ungültige Werte entweder abweisen (Fehlermeldung und Abbruch) oder "passend machen".

Mit "passend machen" ist gemeint: Wenn jemand eine negative Tageszahl oder Monatszahl eingibt, wird der Wert auf 1 gesetzt, und wenn jemand eine zu große Zahl eingibt, wird der Wert auf die größte Tageszahl (31) oder auf die größte Monatszahl (12) gesetzt. Es erfolgt dann keine Fehlermeldung, und das Programm bricht nicht ab, sondern arbeitet mit dem passend gemachten Wert weiter.

Übung 3.1.2#1

Erweitern Sie die drei Setter-Methoden so, dass sie falsche Eingaben passend machen (wie im Text beschrieben).

  • Tageszahl: 1 <= tag <= 31
  • Monatszahl: 1 <= monat <= 12
  • Jahreszahl: 1 <= jahr <= 2500

Dazu sollten Sie einfache if-else-Anweisungen einsetzen. Fortgeschrittene versuchen natürlich, hierbei den ternären Operator einzusetzen.

Übung 3.1.2#2

Ergänzen Sie die Klasse Datum um eine Getter-Methode

public String getDatumString()

die das jeweilige Datum als String zurückliefert, zum Beispiel "3.12.2025".

Übung 3.1.2#3

Beachten Sie das Prinzip der alleinigen Verantwortung (Single Responsibility Principle) und lagern Sie die Konsolen-Ausgabe in eine eigene Klasse DatumsAusgabe aus. Die einzige Verantwortung dieser neuen Klasse ist die Ausgabe des Datums (Rückgabewert von getDatumString() ) auf die Konsole.

Übung 3.1.2#4

Die Methode getDatumString() liefert das Datum als String zurück. Dabei werden auch unschöne Zeichenketten wie "1.3.2025" produziert. Üblicher ist aber eine Angabe mit führenden Nullen wie "01.03.2025". Ergänzen Sie die Methode getDatumString() entsprechend!

Übung 3.1.2#5

Wenn Sie das alles geschafft haben, bringen Sie die folgende Testklasse zum Laufen:

public class TesteDatum
{
    DatumsAusgabe konsole;
    
    public TesteDatum()
    {
        konsole = new DatumsAusgabe();
        konsole.ausgeben(new Datum(17,10,2025));
        konsole.ausgeben(new Datum(17,13,2025));
        konsole.ausgeben(new Datum(34,13,2025));
        konsole.ausgeben(new Datum(-5, 0,2025));
    }
}						

3.1.3 Das Prinzip der alleinigen Verantwortung

Das sogenannte Single Responsibility Principle (SRP) besagt ungefähr Folgendes: Ein Modul (also in Java eine Klasse) sollte eine einzige Verantwortung haben. Umgekehrt sollte jede Verantwortung nur durch ein Modul implementiert werden.

Das Prinzip der alleinigen Verantwortung

Weitere Einzelheiten dazu finden Sie auf dieser Seite in der Abteilung "Begriffe und Konzepte" auf dieser Homepage

Die Ausgabe des Datums hatten wir bereits in die Klasse DatumsAusgabe ausgelagert. Aber die Klasse Datum hat noch mehrere Verantwortungen: Sie verwaltet einmal die Instanzvariablen tag, monat und jahr mit den entsprechenden Setter-Methoden. Statt normaler Getter-Methoden hatten wir dann eine sondierende Methode getDatumString() implementiert. Das ist auch noch in Ordnung, solange es bei der Ausgabe TT.MM.JJJJ bleibt. Wenn aber auch andere Datums-Formate gewünscht sind, müssten wir die Klasse Datum um entsprechende weitere Methoden ergänzen, was aber nicht im Sinne des SRP ist.

Daher die nächste Aufgabe:

Version 3

Übung 3.1.3 #1

a) Lagern Sie die Erzeugung des formatierten Datum-Strings DD.MM.YYYY in eine eigene Klasse DatumFormatierer aus. Die Methode für das Format DD.MM.YYYY könnte diese Signatur haben:

public String getDDMMYYYY(Datum d)

Das Datum, das formatiert werden soll, wird dann als Parameter an die Methode übergeben; das formatierte Datum wird als String zurückgegeben.

b) Ergänzen Sie diese Klasse dann um drei Methoden, die andere Formate für das Datum zurückliefern:

  1. MM/DD/YYYY ist das US-amerikanische Format
  2. DD/MM/YYYY ist das britische Format.
  3. YYYY-MM-DD ist der international gebräuchliche ISO-Standard

Damit das Ganze funktioniert, müssen Sie die Klasse Datum noch mit drei Getter-Methoden für den Tag, den Monat und das Jahr ausstatten. Die alte sondierende Methode getDatumString() kann dagegen gelöscht werden, weil das jetzt ja die Aufgabe der neuen Klasse DatumFormatierer ist.

Hier könnte man nun argumentieren, dass ja doch mehrere Klassen geändert werden müssen, wenn sich die Anforderungen ändern. Hätte man aber die Klasse Datum von vorne herein mit den Getter-Methoden ausgestattet, wie es sich eigentlich gehört, wäre das nicht notwendig gewesen.

Auch die Klasse DatumsAusgabe muss modifiziert werden, damit alles funktioniert. Die Testklasse muss dagegen nicht verändert werden.

/*
 * Diese Klasse ist ausschließlich für die Ausgabe eines Datum-Strings zuständig 
 * (Single Responsibility Principle)
 */

public class DatumsAusgabe
{
    private DatumFormatierer daform = new DatumFormatierer();

    public void ausgeben(Datum d)
    {
        System.out.println("Datum = " + daform.getDDMMYYYY(d));
    }
}

Diese nachträglichen Änderungen der Klassen Datum und DatumsAusgabe wären nicht nötig gewesen, wenn man das ganze Projekt vorher nach dem Top-Down-Prinzip gut durchgeplant hätte.

3.1.4 Auslagerung der Regeln

Version 4

Die Klasse Datum hat immer noch zwei Verantwortungen, nämlich einmal die Verwaltung der Daten und zum anderen das Überprüfen von Tag, Monat und Jahr. Natürlich könnte man beide Verantwortungen in dieser Klasse belassen, viele Entwickler würden das sogar ohne mit der Wimper zu zucken machen. Ich selbst hätte damit auch kein Problem. Sie sollen sich aber hier mit dem wichtigsten Prinzip der OOP beschäftigen, dem SRP. Ob Sie es dann später immer streng anwenden, ist Ihre Sache, aber zumindest muss man es einmal durchgespielt haben.

Also werden wir jetzt die Überprüfungen von Tag, Monat und Jahr aus der Klasse Datum herausnehmen und in eine neue Klasse Datumsregeln übernehmen.

Hier ein Ausschnitt aus dem Quelltext zum Kopieren in die Zwischenablage:

/*
 * Diese Klasse ist ausschließlich für die Datums-Regeln zuständig 
 * (Single Responsibility Principle)
 * 
 * Wenn diese Regeln erweitert werden (Monate mit unterschiedlicher Tageszahl,
 * Berücksichtigung von Schaltjahren), dann muss nur diese Klasse
 * angepasst werden, und sonst keine andere.
 */
public class Datumsregeln
{
    public int korrigiereMonat(int monat)
    {
        if (monat < 1)  return 1;
        if (monat > 12) return 12;
        return monat;
    }

    // und so ähnlich für Tag und Jahr ...
}

Übung 3.1.4 #1

Erstellen Sie diese Klasse und vervollständigen Sie die noch fehlenden zwei Methoden für den Tag und das Jahr.

Im folgenden Text sehen Sie einen Ausschnitt aus der modifizierten Klasse Datum. Vervollständigen Sie diese Klasse dann ebenfalls.

public class Datum
{
    int tag, monat, jahr;
    DatumsRegeln  regeln;

    public Datum(int tag, int monat, int jahr)
    {
        regeln = new DatumsRegeln();
        setTag(tag);
        setMonat(monat);
        setJahr(jahr);
    }

    public void setTag(int tag)
    {
        this.tag = regeln.korrigiereTag(tag);
    }

Wir haben jetzt nur die Regeln für Tag, Monat und Jahr ausgelagert, und die Klasse Datum musste angepasst werden. Die anderen Klassen, DatumFormatierer, DatumsAusgabe und TesteDatum mussten nicht verändert werden.

Falls sich jetzt die Regeln für die Tage, Monate und Jahre einmal ändern sollten, müssen wir nur die neue Klasse DatumsRegeln modifizieren, keine andere Klasse.

Gut, man könnte nun natürlich kritisch einwenden: Wenn wir den gesamten Quelltext in der Klasse Datum gelassen hätten, müssten wir auch nur diese eine Klasse verändern. Das ist völlig richtig, aber wenn man alle möglichen Aufgaben in dieser Klasse Datum bestehen lässt, wird die Klasse immer länger und unübersichtlicher.

Robert C. Martin vergleicht in seinem Buch "Clean Code" das Ganze mit einem Werkzeugkasten. Stellen Sie sich vor, Sie sind Handwerker und besitzen 150 verschiedene Werkzeuge aus 15 unterschiedlichen Kategorien. Wann finden Sie den gesuchten Schraubendreher mit bestimmten Eigenschaften schneller wieder? Wenn Sie alle 150 Werkzeuge in eine große Kiste mit nur einem Fach packen, oder wenn Sie eine spezielle Werkzeugtasche mit 15 Fächern (für jede Kategorie eins) haben, die vielleicht sogar noch weiter unterteilt sind?

3.1.5 Neue Regeln

Version 5-1

Wir wollen die Regeln zur Überprüfung des Datums nun verfeinern. Nicht jeder Monat hat 31 Tage. Ein Monat hat sogar nur 28 Tage, und das auch nur in drei von vier Jahren.

Übung 3.1.5 #1

Ergänzen Sie die Klasse DatumRegeln um eine Getter-Methode

public int tageImMonat(int monat)

die die korrekte Tageszahl des Monats zurückliefert. Schaltjahre müssen Sie hier noch nicht berücksichtigen, das kommt in einer späteren Aufgabe. Der Februar hat also grundsätzlich 28 Tage.

Übung 3.1.5 #2

Nutzen Sie jetzt die neue Methode aus der letzten Übung, um die Methode korrigiereTag() zu verbessern. Ein fehlerhaftes Datum wie 34.2.2025 wird dann zu 28.2.2025 verbessert und nicht mehr zu 31.2.2025 wie in den vorherigen Versionen.

Aufgabe/Übung 3.1.5 #3

Im Konstruktor der Klasse Datum muss jetzt stehen:

        regeln = new DatumRegeln();
        setJahr(jahr);
        setMonat(monat);
        setTag(tag);

Begründen Sie, wieso es notwendig ist, jetzt erst das Jahr, dann den Monat und schließlich den Tag zu überprüfen und nicht umgekehrt.

Bauen Sie diese Änderungen in die Klasse Datum ein.

Version 5-2

Übung 3.1.5 #4

  1. Wenn ein Jahr durch 4 teilbar ist, handelt es sich um ein Schaltjahr.
  2. Es sei denn, das Jahr ist durch 100 teilbar, dann liegt kein Schaltjahr vor.
  3. Es sei denn, das Jahr ist durch 400 teilbar, dann liegt doch wieder ein Schaltjahr vor.

Ergänzen Sie die Klasse DatumRegeln um eine Getter-Methode

public boolean istSchaltjahr(int jahr)

die true zurückliefert, wenn es sich bei dem Parameter jahr um ein Schaltjahr handelt.

Modifizieren Sie dann die Methode korrigiereTag() entsprechend, so dass ein Schaltjahr-Februar maximal 29 Tage hat.

Ergänzen Sie dann die Testklasse so, dass Sie die neuen Regeln überprüfen können.

Seitenanfang
Weiter mit der switch-case-Anweisung ...