Die verborgene Schätze von Faktor-IPS: Eigene Datentypen definieren

Neben Standard-Java-Primitiven und Wertetypen wie int und Double gibt es ein paar vordefinierte Datentypen wie Money (ein dezimaler Betrag mit fester Genauigkeit, kombiniert mit einer Währung) und die Möglichkeit in Faktor-IPS definierte Aufzählungen als Datentypen zu verwenden.

Veröffentlicht:

Kategorie: Produkt Updates

Ein Bild unseres Maskottchens Fipsi als Pirat

In dieser Folge der verborgenen Schätze von Faktor-IPS möchte ich zeigen wie einfach es ist eigene Datentypen zu erstellen und einige Anregungen geben, welche Datentypen man verwenden könnte. Das Konzept der Datentypen und welche von Faktor-IPS direkt unterstützt werden eklärt dieses PDF. Neben Standard-Java-Primitiven und Wertetypen wie int und Double gibt es ein paar vordefinierte Datentypen wie Money (ein dezimaler Betrag mit fester Genauigkeit, kombiniert mit einer Währung) und die Möglichkeit in Faktor-IPS definierte Aufzählungen als Datentypen zu verwenden. Im PDF gibt es auch ein kleines Beispiel für einen eigenen IsoDate Datentypen, das ich hier etwas näher betrachten möchte:

public class IsoDate {
    private int year;
    private int month;
    private int day;
 
    public IsoDate(int year, int month, int day) {
        this.year = year;
        this.month = month;
        this.day = day;
    }
 
    public int getYear() {
        return year;
    }
 
    public int getMonth() {
        return month;
    }
 
    public int getDay() {
        return day;
    }
 
    public String toString() {
        String m = month < 10 ? "0" + month : "" + month;
        String d = day < 10 ? "0" + day : "" + day;
        return year + '-' + m + '-' + d;
    }
 
    @Override
    public int hashCode() {
        // TODO
    }
 
    @Override
    public boolean equals(Object obj) {
        // TODO
    }
 
    public static final IsoDate valueOf(String s) {
        if (s == null || s.equals("")) {
            return null;
        }
        String[] tokens = s.split("-");
        int year = Integer.parseInt(tokens[0]);
        int month = Integer.parseInt(tokens[1]);
        int date = Integer.parseInt(tokens[2]);
        return new IsoDate(year, month, date);
    }
}

 

Der Datentyp ist ein Objekt mit ein paar Attributen, das in einen String geschrieben und aus ihm instatiiert werden kann. Um den Datentypen zu verwenden muss der <DatatypeDefinitions>-Section der .ipsproject-Datei ein Eintrag hinzugefügt werden:

Da der Datentyp direkt im Modellprojekt definiert ist muss noch javaProjectContainsClassesForDynamicDatatypes="true" im Wurzeltag <IpsProject> gesetzt werden.

Aber um ehrlich zu sein ist noch eine Datumsklasse so ziemlich das letzte was die Java-Welt braucht. Welche anderen Beispiele können wir finden? Es gibt einige weithin genutzte Klassen im JRE, die aus einem String instantiiert werden können. Nehmen wir zum Beispiel java.util.Currency. Ein Währungsobjekt wird aus einem ISO 4217 Währungscode erstellt der auch von toString zurückgegeben wird.

Wir können noch einen Schritt weiter gehen. Währungen sind Wertobjekte (jede Instanz mit dem gleichen Währungscode ist funktional gleich). Dies können wir unserem Datentypen als Attribut hinzufügen(valueObject="true"). Jetzt können wir die Währung in Faktor-IPS verwenden, aber wir müssen die ISO 4217 Währungscodes kennen und als String eintippen um sie zu benutzen. Wäre es nicht besser, aus einer Liste aller bekannter Währungen auswählen zu können? Wenn man die Dokumentation der DatatypeDefinitions in der .ipsproject-Datei betrachtet findet man eine Eigenschaft isEnumType mit zugehöriger getAllValuesMethod. Erstere auf true gesetzt un zweitere auf getAvailableCurrencies und schon laufen wir in eine unschöne java.lang.ClassCastException: java.util.HashSet cannot be cast to [Ljava.lang.Object;. Leider erwartet Faktor-IPS ein Array als Rückgabewert. Keine Sorge, weiter unten gibt es auch für dieses Problem eine Lösung, aber zuerst möchte ich eine andere Klasse betrachten, die besser auf die Erwartungen passt; java.util.TimeZone zum Beispiel:

Et voilá, ein neuer IPS-Datentyp ohne eine einzige Zeile Javacode.

Die meisten Projekte haben bereits einige Aufzählungen für andere Systeme definiert, die sie gerne auch mit Faktor-IPs weiter verwenden möchten – nichts leichter als das. Nehmen wir als Beispiel die Sparten einer Versicherung:

Der zugehörige Datentyp sähe zum Beispiel so aus:

Aber wenn man diesen Datentypen in Faktor-IPS nutzt sieht das mit den komplett groß geschriebenen Enum-Literalen nicht all zu gut aus. Vielleicht hat unsere Aufzählung ja auch schon ein nutzerfreundlicheres String-Attribut das wir verwenden können:

Damit können wir Faktor-IPS anweisen den displayName zur Anzeige zu verwenden und die Enum-Literale lediglich im Code:

Damit zurück zum Währungsdatentyp. Da wir die Klasse Currency nicht verändern können haben wir zwei Möglichkeiten:

  1. Wir können einen org.faktorips.datatype.Datatype nebst zugehörigem org.faktorips.codegen.DatatypeHelper erstellen, die sich um die Erzeugung des passenden Zugriffscodes kümmern. Dazu später mehr.
  2. Wir können sie in unsere eigene Klasse verpacken, deren Methoden die erwartete Signatur bedienen.

Zuerst die zweite Option:

Damit können wir den Datentypen so definieren:

Nur müssen wir jetzt immer mit Waehrungs- statt mit Currency-Objekten arbeiten und diese an Schnittstellen ein- und auspacken. Gut genug für ein Beispiel und hoffentlich etwas das zu eigenen Datentypen anregt, aber vielleicht ist hier doch Variante 1 die richtige.

Wenn wir die Datentyp-Klasse nicht verändern können müssen wir hoffen, dass ihre Signaturen stimmen. Aber was ist mit unseren eigenen Klassen, die wir anpassen können? Welche Klassen fallen einem da ein? Ein Beispiel könnten unsere Faktor-IPS-Produktbausteine sein. So mancher wünschte sich schon, diese direkt als Schlüssel in einer Tabelle zu verwenden. Also verwandeln wir eine Produktbaustein-Klasse in einen Datentypen. Dazu brauchen wir

  1. eine Möglichkeit einen Produktbaustein in einen String zu verwandeln (einfach; es gibt bereits die Methode getId)
  2. eine Möglichkeit ihn aus diesem ID-String zu erzeugen (wozu wir wohl ein RuntimeRepository brauchen)
  3. die Liste aller verfügbaren Produktbausteine, um diese direkt auswählen zu können statt fehleranfällig und unvalidiert Runtime-IDs einzutippen (auch hier sollte das RuntimeRepository mit seiner Methode getAllProductComponents hilfreich sein)
  4. möglicherweise einen besser zur Anzeige geeigneten Namen (den wir einfach mit einem eigenen Produktattribut definieren können)

Die Punkte 1 und 4 sind offensichtlich leicht zu erfüllen, aber wie kriegen wir ein RuntimeRepository in unsere statischen Methoden? Eine kleine Helferklasse scheint angebracht. Mit dem Namen der Table-of-Contents-Datei können wir ein neues ClassloaderRuntimeRepository erzeugen. Da wir es in einem statischen Kontext halten und nicht jedes Mal wenn wir einen neuen Produktbaustein anlegen oder ändern unseren Arbeitsbereich neu laden möchten erstellen wir einen Manager für das Repository der prüft, ob der ToC aktualisiert wurde.

Damit ist es einfach, eine normale Produktbausteinklasse zum Datentyp zu machen:

Und schon können wir unsere Produktbausteine als Datentypen verwenden, zum Beispiel als Schlüssel in einer Preistabelle. Natürlich funktioniert dieses Beispiel so nur in einem kombinierten Modell- und Produktprojekt; um mit getrennten Projekten und Deployments klar zu kommen brucht es noch etwas Arbeit, aber das sei dem Leser zur Übung gelassen, wie mein Professor zu sagen pflegte.

Zum Schluss möchte ich noch auf die „echten“ Datentypen eingehen, die in einem eigenen Plugin definiert werden und für die angepasster Code generiert wird, um einen möglicherweise noch besseren Währungs-Datentyp zu erhalten. Als erstes muss in Eclipse ein Plugin-Projekt angelegt werden, in dem eine Abhängigkeit zu org.faktorips.devtools.core und eine Erweiterung zu org.faktorips.devtools.core.datatypeDefinition angelegt werden. Dazu benötigen wir zwei Klassen: Den Datentyp und einen zugehörigen Helper. Der Datentyp leitet von GenericValueDatatype ab und implementiert EnumDatatype. Im Konstruktor setzen wir den qualifizierten Namen (wie wir ihn im XML bei id eingetragen hatten), den Namen der Methode zur Erzeugung eines Wertobjekts("getInstance", wie zuvor) und den Namen der Methode die prüft, ob ein gegebener Wert geparst werden kann (auf null, da Currency keine solche Methode hat). Wir müssen Methoden implementieren, die alle Werte zurückgeben (wie in unserer Wrapper-Klasse) und die parsbarkeit prüfen(dazu versuchen wir einfach eine Currency anzulegen und geben im Fehlerfall false zurück). Der Helper ist nur ein leerer GenericValueDatatypeHelper für unseren Datentyp.

Jetzt haben wir Javas Currency-Klasse als Faktor-IPS-Datentyp und müssen nicht selbst eine Liste mit allen Währungen pflegen.

Update 22.6:

Faktor-IPS 22.6 bringt einige Verbesserungen für selbstdefinierte Datentypen:

  • Die Methode, die alle Werte zurückgibt, kann nun statt einem Array auch eine Collection als Rückgabewert haben.
  • Auch Nicht-Aufzählungs-Datentypen können neben einer ID einen Namen haben.
  • Currency ist ein offiziell in Faktor-IPS unterstützter Datentyp
  • Header des Faktor-IPS Release 23.6.

    Faktor-IPS Release 23.6

    In den letzten Monaten wurde Faktor-IPS fleißig von unserem Produktteam weiterentwickelt. In diesem Beitrag finden Sie die wichtigsten Informationen sowie alle relevanten Links:…

  • Sechs Waben mit den Faktor Zehn Produkten im Hintergrund und der Aufschrift Release 22.12 in grün

    Winterrelease 22.12

    Um den Jahreswechsel haben wir die Version 22.12 der weiter gewachsenen Faktor-Zehn-Suite veröffentlicht: Finden Sie in diesem Beitrag alle relevanten Links.

  • Bild zu Release Summit 22.12

    Release Summit 22.12

    Unser halbjährlicher Austauschtermin zum neuen Release der Faktor Zehn Produktentwicklung hat wieder stattgefunden.