JasperReports ist eine beliebte
Java-Bibliothek zur Erstellung von Reports, in der Regel in PDF oder
HTML-Form. Reports sind Auszüge oder Zusammenfassung aus größeren
Datenbeständen in Form von Tabellen, Diagrammen und begleitenden
Texten.
JasperReports bietet nun unter anderem eine API an, mit der man
programmatisch einen Report zusammenbauen kann. Diese API lässt aber
einiges zu wünschen übrig, was auch die Macher der Bibliothek
DynamicJasper erkannt haben, die aber
immer noch sehr imperativ ist. Das hat uns dazu veranlasst eine rein
funktionale API in
Scala zu implementieren,
die auf JasperReports aufbaut, aber den Prinzipien der
Nicht-Mutierbarkeit und der Kompositionalität folgt.
In diesem Beitrag demonstriere ich die Probleme mit der
JasperReports-API, wie sie gelöst wurden, und welche funktionalen
Grundprinzipien beim Design von APIs beachtet werden sollten.
Als Einstieg direkt ein kleines Beispiel für die Verwendung der
JasperReports-API, in Java:
JRDesignBand myCompanyBanner() {
JRDesignBand band = new JRDesignBand();
band.setHeight(30);
JRDesignStaticText t = new JRDesignStaticText();
t.setFontName("Helvetica");
t.setFontSize(12);
t.setHeight(12);
t.setWidth(200);
t.setX(0);
t.setY(0);
t.setText("My Company");
band.addElement(t);
return band;
}
JasperDesign myReport() {
JasperDesign d = new JasperDesign();
d.setName("myreport");
JRDesignBand banner = myCompanyBanner();
d.setPageHeader(banner);
return d;
}
Der Code definiert zunächst eine Funktion die ein sogenanntes
Band mit dem Schriftzug unserer Beispiel-Firma erzeugt und
zurückgibt. Ein Band ist in JasperReports eine Art Abschnitt des
Reports, der immer die volle Seitenbreite, aber nur eine bestimmte
Höhe einnimmt. Viele Band-Elemente hintereinander gehängt ergeben den
gesamten Report. Anschließend nutzt die Funktion myReport dieses
Firmen-Banner als Kopfzeile eines ansonsten leeren Reports.
CRUD vs. Nicht-Mutierbarkeit
Wie man sieht ist die JasperReports-API imperativ gestaltet,
indem die Komponenten dem sogenannten CRUD-Pattern folgen: CRUD steht
für Create-Read-Update-Delete, und zeigt sich hier darin dass alle
Objekte zunächst „leer“ erzeugt werden, und anschließend mit vielen
Set- und Add-Funktionen mit dem Inhalt gefüllt werden müssen, den wir
haben wollen.
Das ist, für einen funktionalen Programmierer, aber zunächst mal nur
lästig und fördert einen unübersichtlichen „flachen“ Code (siehe auch
hier
für ein längeres Beispiel in den JasperReport-Sourcen). Allerdings
wird das in sehr vielen APIs dieser Art an der einen oder anderen
Stelle zum Problem. Entweder dadurch, dass „Back-References“
hinzugefügt werden: ein Beispiel dafür ist in der weit verbreiteten
API XML-DOM zu finden. Jedes
XML-Element hat dort eine Referenz auf den Vater-Knoten im XML-Baum.
Dadurch kann man das selbe XML-Element-Objekt nicht an mehrere Stellen
in den XML-Baum hängen. Das führt zu umständlichen Abstraktionen und
dazu, dass viel zu oft tiefe Kopien und „Imports“ von Knoten gemacht
werden, um auf „Nummer sicher“ zu gehen.
Eine andere Folge des CRUD-Patterns ist, dass Bibliotheks-Entwickler
offenbar zu gerne noch weiteren interen Zustand in die (ohnehin schon)
mutierbaren Objekte einfügen. Das sieht man den Klassen und Objekten
dann überhaupt nicht mehr an, und führt im besten Fall noch zu einem
Kommentar in der Referenz-Dokumentation, wie beispielsweise in der
Klasse
GridData
aus dem Eclipse-Projekt, oft aber zu obskuren Fehlern.
Beispiel: Styles
In jedem erzeugten Report-Element immer wieder neu die Schriftarten,
Abstände, Rahmen und viele weitere Eigenschaften, die das Aussehen
betreffen, zu setzen ist selbstverständlich nicht praktikabel. Was wäre
zum Beispiel, wenn wir über den Stil abstrahieren möchten. Wenn man in
die API-Referenz guckt, findet man:
public void setStyle(JRDesignStyle style)
Und die Klasse JRDesignStyle scheint alles zu enthalten was wir
brauchen, also schreiben wir doch eine Funktion die ein solches
Style-Objekt erzeugt, and ändern unsere Funktion myCompanyBanner
folgendermaßen:
JRDesignStyle boldSmallText() {
JRDesignStyle st = new JRDesignStyle();
st.setName("bold-small");
st.setFontName("Helvetica");
st.setFontSize(12);
st.setBold(true);
return st;
}
JRDesignBand myCompanyBanner() {
...
JRDesignStaticText t = new JRDesignStaticText();
JRDesignStyle st = boldSmallText();
t.setStyle(st);
t.setHeight(12);
...
}
Aber was passiert wenn man aus dem ereugten Report ein PDF generieren will:
net.sf.jasperreports.engine.JRRuntimeException: Could not resolve style(s): bold-small
...
Was ist passiert? Jasper will den Namen ‚bold-small‘ auflösen, und
konnte das nicht? Tatsächlich ist es so, dass für das Aussehen des
Text-Elements die Eigenschaften des Style-Objekts, das an setStyle
übergeben wird, überhaupt keine Rolle spielen! Die einzige Eigenschaft
die er davon nutzt ist der Name des Stils. Der Gesamt-Report, das
JasperDesign-Objekt, hat dann wiederum eine „globale“ Liste von
Style-Objekten. In diesem sucht die JasperReports-Engine nach einem
Objekt mit dem selben Namen und nur dessen Eigenschaften sind dann
relevant um das Text-Element auszugestalten.
Man kann also diese Styles nicht verwenden, wenn man gleichzeitig die
konkrete Gestalt des Firmen-Banners in einer Funktion „verstecken“
will. Entweder muß man der Funktion übergeben welche Styles sie
verwenden kann, oder sie muß zurückgeben welche sie verwendet hat. Das
freie „Kapseln“ ist aber gerade eine Essenz der funktionalen
Programmierung - man ruft eine Funktion auf die ein Band erzeugt, und
kann dieses Band frei verwenden ohne sich darüber Sorgen machen zu
müssen wie es entstanden ist, oder wo es sonst noch verwendet wird.
Eine kompositionale API
Diese Problemen für die Nutzer einer API, beziehungsweise
Fettnäpfchen für die Weiterentwicklung einer Bibliothek, kann man sehr
leicht vermeiden, indem man die Schnittstellen rein funktional
gestaltet. Objekte sollten nicht mutierbar sein, also nach der
Erzeugung nicht mehr verändert werden können. Dies erleichtert das
Verständnis der API, ermöglicht eine freie (Wieder-) Verwendung der
Objekte, erleichtert Tests und ermöglicht nicht zuletzt den Zugriff
auf die Objekte aus mehreren Threads heraus.
Eine weitere Eigenschaft der Nicht-Mutierbarkeit ist, dass über die
Konstruktoren bereits alle Eigenschaften eines Objekts gesetzt werden
können. Dadurch ergibt sich eine Verschachtelung des Codes, die direkt
die resultierende Baum-Struktur der Report-Elemente wiederspiegelt.
Dies ist wesentlich lesbarer und verständlicher als die flache
Struktur des Java-Codes:
val myCompanyBanner = Band(
height = 30 px,
content = StaticText(
x = 0 px,
y = YPos.float(0 px),
width = 200 px,
height = Height.fixed(12 px),
text = "My Company"
)
)
val myReport = Report(
name = "myreport",
page = Page(
header = Some(myCompanyBanner)
)
)
Diese Beschreibung des Reports und seiner Elemente, transformiert
unsere Bibliothek anschließend in ein Report-Objekt aus der
Jasper-Bibliothek, das dann weiter verarbeitet werden kann.
Der obige Beispielcode ignoriert noch das Thema der Schriftart, bzw.
der Stils, was wir in folgendem Abschnitt nachholen.
Styles
In der funktionalen Bibliothek, die wir entwickelt haben, gibt es das
oben erwähnte Problem mit den Styles nicht. Man kann Styles frei
definieren, kombinieren und verwenden, ohne sich darüber Gedanken zu
machen wo, wie oft, und in welchen Reports sie verwendet werden:
val boldSmallText = Style(
font = Font(
fontName = Some("Helvetica"),
fontSize = Some(12),
bold = Some(true)
)
)
val myCompanyBanner = Band(
height = 30 px,
content = StaticText(
...
style = boldSmallText,
text = "My Company"
)
)
Entscheidend ist die sogenannte Komponierbarkeit oder
Kompositionalität der Elemente. Der Darstellungsstil ist durch das
Text-Element selbst definiert, und hängt nicht davon ab in welchen
Report es eingebaut wird. Das ermöglicht dann auch die Abstraktion, in
dem Sinn dass man hier nicht wissen muss aus was myCompanyBanner
besteht, um es benutzen zu können.
Kompositionalität
Zum Abschluss noch etwas theoretischer Hintergrund:
Der Begriff Kompositionalität stammt aus der Semantikforschung Anfang
des 20. Jahrhunderts, und ist dort auch als das
Frege-Prinzip bekannt.
Er steht dafür, dass sich die Bedeutung eines komplexen Ausdrucks allein aus
den Bedeutungen der Teilausdrücke und den Kombinatoren zusammensetzt,
mit denen diese Teilausdrücke zum Gesamtausdruck zusammengefügt sind.
Andersherum bedeutet es damit auch, dass sich die Bedeutung eines
Ausdrucks nicht ändert, wenn er in verschiedenen Kontexten in größeren
Ausdrücken verwendet wird.
Daraus folgt eine wichtige Eigenschaft für das Programmieren, nämlich
dass man „Gleiches durch gleiches ersetzen“ kann. Bei einer
kompositionalen API macht es keinen Unterschied, ob man ein neues
Objekt mit gewissen Eigenschaften erzeugt, oder ein bestehendes
gleiches Objekt wiederverwendet. Dies ist das Sprungbrett für
mächtige Funktionen und Tools, die einem das Erstellen von Reports
noch weiter erleichtern können.
ScalaJasper
Diese und weitere Probleme der JasperReports-API haben wir mit
ScalaJasper bereinigt;
teils durch Erweiterung um neue Features, aber genauso auch durch
Entfernen von „Irrwegen“ aus der JasperReports-API.
Der Sourcecode der Bibliothek ist seit kurzem frei zugänglich bei
GitHub, wobei sich bis zum ersten Release (hoffentlich in den nächsten
Wochen) noch immer viel verändern kann - wozu auch der Name gehört.