Home > Informatik > Einf. in die OPP mit Java > Java-Beispiele > Datumsberechnungen

Workshop "Das Projekt Datum"

1. 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

Dieser Quelltext 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.

Übung

  1. Erstellen Sie eine Klasse Datum mit drei Instanzvariablen tag, monat und jahr.
  2. Schreiben Sie entsprechende Setter- und Getter-Methoden für die Instanzvariablen.
  3. Implementieren Sie einen Konstruktor

    public Datum(int tag, int monat, int jahr)

    der die drei Parameter an die drei Setter-Methoden übergibt, aber sonst nichts macht.

Wir haben jetzt also eine ganz einfache Klasse Datum erstellt, mit der neue Objekte erzeugt werden können. Allerdings führt der Konstruktor beim Erzeugen neuer Datums-Objekte noch keinerlei Überprüfungen durch. Er übergibt den Parameter tag einfach an die Setter-Methode setTag(). Es ist also durchaus möglich, ein Datum wie den -13.14.5000 zu erzeugen. Mit getTag() würden Sie dann den Wert -13 erhalten.

Version 2

Die drei Setter-Methoden liegen in der Version 1 erst in einer Art "Rohfassung" vor. Ihre Aufgabe ist 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. Aus dem falschen Datum -13.14.5000 würden die Setter-Methoden also das korrigierte Datum 1.12.2500 erzeugen, falls Sie festgelegt haben, dass 2500 die maximale Jahreszahl ist.

Übung

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

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

public String getDatumString()

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

Übung 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 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 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));
    }
}						

Exkurs: 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 6

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.

2. 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 7

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. 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 8

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 9

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 10

Im Konstruktor der Klasse Datum muss jetzt stehen:

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

Begründen Sie, wieso es notwendig ist, 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 11

  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.