Die ersten drei Beiträge dieser Reihe handelten von einer Java API zur Feiertagsberechnung. In diesem Beitrag geht es um eine nachtragliche Verbesserung der Internationalisierung (I18N) und der minimalinvasiven Implementierung mit Hilfe des Decorator Design-Pattern.
LocalDate date = LocalDate.of(2020, 12, 25) Holidays holidays = Holidays.in(UnitedKingdomCountryOrProvince.NIR); String name = holidays.getHoliday(date).orElse("-"); // "Christmas Day"
Der hier dargestellte Dreizeiler bestimmt den Feiertagsnamen für den 25.12.2020 in der Provinz Nord-Irland. Es verwundert vermutlich nicht sehr, dass als Result "Christmas Day"
zurückgegeben wird. Die ursprüngliche Version der API arbeitete mit festen Namen für die Feiertage, damit war die Umsetzung einfach, aber mit einigen Einschränkungen belastet.
Die auffälliste Einschränkung ist die schlechte Internationalisierung der Lösung. Im oberen Beispiel wird der Name "Christmas Day"
zurückgegeben, obwohl als Standardsprache Deutsch eingestellt ist. In diesem Fall hätte das Ergebnis "1. Weihnachtstag"
lauten sollen.
Ein Methode mit zusätzlichem Parameter für die gewünschte Übersetzungsprache muss noch ergänzt werden. Die Methode ohne zusätzlichen Locale
Parameter für die Sprache verwendet die jeweilige Standardsprache des Systems.
LocalDate date = LocalDate.of(2020, 12, 25) // "1. Weihnachtstag" Holidays.in(NIR).getHoliday(date); // Default-Locale Locale.GERMANY // "Christmas Day" Holidays.in(NIR, Locale.UK).getHoliday(date); // "Christtag" Holidays.in(NIR, new Locale("de", "AT").getHoliday(date);
Die Entscheidung, den Locale
Parameter der in
Methode hinzuzufügen, beruht auf den bisherigen Anwendungsfällen, viele Feiertage in einer Sprache zu erfragen. Bei anderen Anfordungen könnte der Locale
Parameter aber auch in die getHoliday
und getHolidays
Methoden wandern.
Übersetzungen
Damit die Feiertagsnamen übersetzt werden können, benötigt die angepasste API ein entsprechendes Resource-Bundle. In ihm müssen alle, im System bekannten, Feiertage als Key-Value Paar vorhanden sein. Der Key ist ein eindeutiger Schlüssel für den Feiertag und der Value eine Übersetzung des Namens.
# holidays.properties nationalfeiertag=Nationalfeiertag # holidays_de.properties new-years-day=Neujahr boxing-day=2. Weihnachtsfeiertag # holidays_de_DE.properties boxing-day=2. Weihnachtsfeiertag # holidays_de_AT.properties boxing-day=Stefanitag
Im obigen Beispiel sind Beispielzeilen aus verschiedenen Dateien des Resource Bundles dargestellt.
In der Basis-Datei holidays.properties
findet sich der Eintrag nationalfeiertag
für den österreichischen Nationalfeiertag. Für diesen Feiertag bietet das Holidays Projekt keine Übersetzungen an. Daher gilt dieses Eintrag für alle Sprachen.
Da der Name des Neujahrsfeiertag in den beiden unterstützten deutschsprachigen Ländern, Deutschland und Österreich, identisch ist, steht er in der Datei holidays_de.properties
. Hier stehen alle länderübergreifenden deutschen Namen.
Der Name des zweiten Weihnachtsfeiertag unterscheidet sich zwischen Deutschland und Österreich. Daher steht der boxing-day
Eintrag in den Dateien holidays_de_DE.properties
für Deutschland und holidays_de_AT.properties
für Österreich. Wird die Sprache ohne Länderzusatz genutzt, wird nur in holidays_de.properties
gesucht. Daher steht hier auch noch einmal der Feiertagsname und zwar in der wahrscheinlicheren Variante (Deutschland).
Implementierung
Die internen Datenstrukturen müssen entsprechen angepasst werden, dass sie nun statt den Namen, den Key für das Resource Bundle verwenden.
Die bisherige Kernmethode für die Abfrage der Feiertage für ein Land nutzt einen internen Cache für die Feiertagsberechnungen. Den Cache um beliebig viele Sprachvarianten zu erweitern, hat keinen besonderen Charme. Dafür gibt es schon genügend Regionen die den Cache bevölkern könnten.
public static Holidays in(Locale locale) { MapHolidays holidays = map.get(requireNonNull(locale)); if (holidays != null) { return holidays; } return in(locale, getProvider(locale)); }
Hier hilft wieder einmal das Decorator Design-Pattern. Damit ist es möglich eine existierende Klasse ohne Vererbung mit neuen Funktionalitäten auszustatten. Im Beitrag Guard-Decorator wurde z.B. die Auslagerung von Prüfungen in Decorator Implementierungen vorgestellt.
Das bisherige Ergebnis vom Typ Holiday
wird mit einer weiteren Implementierung von Holiday
dekoriert, die sich um die Übersetzung kümmert.
public static Holidays in(Locale locale, Locale language) { MapHolidays holidays = map.get(requireNonNull(locale)); if (holidays != null) { return new LocalizedHolidays(holidays, language); } return new LocalizedHolidays(in(locale, getProvider(locale)), language); }
Die Klasse LocalizedHolidays
wird mit dem gewünschten Locale
für die Sprache initialisiert und lädt die entsprechende ResourceBundle
Instanz.
@Override public Optional<String> getHoliday(LocalDate date) { return wrapped.getHoliday(date).map(bundle::getString); }
Die Methode getHoliday
nimmt das Ergebnis der dekorierten Instanz entgegen und verwendet es als Key für das ResourceBundle. Da der Rückgabewert der Methode ein Optional
ist, kann direkt dessen map
Methode genutzt werden um die Übersetzung zu bestimmen.
Bei der getHolidays
Method ist die Sache etwas komplizierter, denn hier wird nicht ein einzelner Feiertag abgefragt, sondern alle Feiertage des Jahres.
Eine klassische Fehlentwicklung in der Programmierung ist das Umschaufeln von Datenstrukturen. In vielen Anwendung finden sich Methoden, die Werte aus einer Collection umgeformt in eine neue Collection eingefügen. Dabei werden immer wieder neue Objekte erzeugt und die Attribute des einen Types teilweise identisch, teilweise verändert kopiert. Die Attribute werden meist nur kopiert, weil die Konstruktoren der Zielklassen dies verlangen und nicht, weil diese Daten benötigt werden. Auch hier eignet sich das Decorator Pattern um unnötiges Kopieren zu vermeiden.
@Override public Map<LocalDate, String> getHolidays(int year) { return Collections.unmodifiableMap(new LocalizedMap(wrapped.getHolidays(year), bundle)); }
Dieses Mal wird ein Decorator für eine Map
verwendet. Damit niemand versucht in diese Map zu schreiben, wird sie mit Collections.unmodifiableMap
davor geschützt.
private static class LocalizedMap extends AbstractMap<LocalDate, String> { ... private class LocalizedEntry extends SimpleImmutableEntry<LocalDate, String> { private LocalizedEntry(LocalDate key, String value) { super(key, value); } @Override public String getValue() { return bundle.getString(super.getValue()); } } @Override public int size() { return wrapped.size(); } @Override public String get(Object key) { String value = wrapped.get(key); return value == null ? null : bundle.getString(value); } @Override public Set<Entry<LocalDate, String>> entrySet() { return wrapped.entrySet().stream().map(e -> new LocalizedEntry(e.getKey(), e.getValue())) .collect(Collectors.toSet()); } }
Damit die Implementierung kurz bleibt, erweitert die LocalizedMap
die AbstractMap
aus der Java Standard API. Diese erwartet eine Implementierung der entrySet
Methode. Mit ihrer Hilfe lassen sich viele Methoden einer Map realisieren.
Diese Methoden in der AbstractMap
müssen nicht performant sein. Aus diesem Grund sind manche Methoden wie size
und get
hier zusätzlich überschrieben worden. Sie holen sich ihre Informationen direkt aus der dekorierten Map
und gehen nicht den Umweg über das Entry-Set.
Fazit
Damit ist die Implementierung einer verbesserten Internationalisierung auch schon beendet. Es hat sich gezeigt, dass der Einsatz des Decorator Design Pattern, keine große Änderungen in der Gesamtimplementierung erfordert. Nun ist nur noch zu prüfen, ob die Übersetzungen stimmen.