Samstag, 31. März 2012

Ein konfigurierbares Chart in ABAP

Kürzlich hatte ich in einer neu zu entwickelnden Anwendung für Internetauktionen die Aufgabe, für den Manager der Auktion einen sich selbst aktualisierenden Monitor zu programmieren, der den Verlauf der Gebote darstellt.

Da es sich um eine BSP-Anwendung und nicht etwa um ein WebDynpro handelt, hatte ich die volle Freiheit, aus der grossen Menge der im Internet verfügbaren Chart-Tools die Komponente auszuwählen, die sowohl optisch am ansprechendsten als auch vom API her am besten geeignet ist.

Natürlich wäre auch der Internet Graphics Server (IGS), ein mit dem SAP Web AS selbst ausgeliefertes Chart-Tool eine Option gewesen, und andernorts habe ich das auch bereits eingesetzt. Nur habe ich bei diesem, wie bei manchen anderen an sich guten, aber älteren Produkten von SAP kein Vertrauen, dass diese noch weiterentwickelt werden. Ein Grund, sich im Web umzuschauen.

Solange das HTML5-Element <canvas> noch nicht vorausgesetzt werden kann – und auch weil ein für alle gängigen Browser gemeinsamer Nenner gefunden werden sollte [1] – fiel meine Wahl auf die Komponente Open Flash Chart 2. Ich bin zwar kein grosser Fan von Flash und bin der Meinung, dass das meiste, was Flash kann, zu den Standardfunktionen von Browsern gehören sollte - aber das ist Zukunftsmusik. Solange eine solche Funktion noch nicht allgemein verfügbar ist, finde ich es in Ordnung, für spezielle Präsentierungsaufgaben weitverbreitete und wohlbekannte Plugins wie Flash zu verwenden.

Das Ergebnis - in Form eines sogenannten "Multiline Charts" (mit Dummydaten und geschwärzten Auktionsteilnehmernamen) sieht schon ganz passabel aus, ohne dass grosse Veränderungen am vorgeschlagenen Customizing gemacht werden mussten:



Die Daten, die im Chart darzustellen sind, werden mit der JavaScript-DOM-Funktion setInterval periodisch via Ajax vom Server abgefragt:

// Chart alle N Sekunden aktualisieren
window.setInterval(
request_update_chart,
refreshInterval );

Die Abfrage selbst verwendet die Funktion doRequest aus meiner minimalistischen JavaScript-Bibliothek minlib.js, eine schmale Abstraktion der verschiedenen XMLHttpRequest-Implementierungen.

// Wenn das Chart sichtbar ist: Request absetzen
function request_update_chart() {
if (byId("historie_chart").style.display != "none") doRequest(
"gebots_historie?belnr="+getText("belnr"),
do_update_chart );
}

function do_update_chart() {
byId("historie_chart").load( this.responseText );
}

Der Server (also der in ABAP implementierte Behandler für den Ajax-Request) liefert die Daten im JSON-Format zurück. Dabei verwende ich für die Transformation der ABAP-Daten nach JSON den in einem früheren Blog beschriebenen JSON-Builder. Der erzeugte JSON-Hash enthält alle für die Darstellung nötigen Informationen, also nicht nur die Daten der Kurven selbst, sondern auch die Optionen wie z.B. die Farbgebung der Kurven, den Stil, mit dem die Datenpunkte darzustellen sind, usw.:

{
"elements": [
{ "values":[ {"x":1324560861 ,"y":77.6526 },
{"x":1327390414 ,"y":76.4155 },
{"x":1328085458 ,"y":74.5599 },
...
],
"dot-style": {
"tip":"#val# <br>#date:H:i:s#<br>XXXX AG (10)",
"colour":"#0000ff",
"type":"star",
"dot_size":"5"
},
"colour":"#0000ff",
"text":"XXXX AG (10)",
"type":"line",
"width":"4",
"font-size":"10"},
{ "values":[{"x":1324560861 ,"y":93.9605 },
{"x":1324632669 ,"y":93.3419 },
{"x":1324635361 ,"y":92.7234 },
... ],
...},
...
],
"x_axis": {
"labels":
{ "steps":864000.0 ,
"visible-steps":1 ,
"rotate":90,
"text":"#date:j.n.#"},
"min":1324512000,
"max":1330560000,
"steps":864000.0},
"y_axis": {
"labels":{
"steps":10 ,
"visible-steps":1 ,
"rotate":0,
"text":"#val#"},
"min":50.0 ,
"max":110.0 ,
"steps":10 },
"num_decimals":3
}


Da ich meine Kunden kenne, weiss ich, dass es an Präsentierungen immer etwas herumzumäkeln gibt. Man zeigt eine Lösung dem ersten Kunden, und er ist mit den Farben der Linien nicht zufrieden. Der nächste möchte, dass die Datenpunkte auf eine andere Art dargestellt werden. Der dritte möchte wieder zur ursprünglichen Farbwahl zurückkehren, und er hat ebenso gute Argumente dafür wie der erste, der die Farbwahl geändert haben wollte.

In solchen Fällen ist es für einen Entwickler das richtige Verhalten, sich nicht auf diese Diskussionsebene zu begeben, sondern denen, die hier die Stil- und Designexperten sein wollen, die Mittel an die Hand zugeben, die Präsentierung nach eigenen Vorstellungen zu ändern.

Mit anderen Worten habe ich die Regel 38 des Pragmatischen Programmierers angewendet:

38. Put Abstractions in Code, Details in Metadata

Program for the general case, and put the specifics outside the compiled code base.


Für die Chart-Optionen ist eine spezielle Klasse zuständig, die das Interface ZIF_FG_CHART_OPTIONS implementiert. Die Optionen werden im Konstruktor dieser Klasse gesetzt, genauer: in einem im Konstruktor includierten Programm. Dieses Incldue kann mit einer eigenen Transaktion separat gepflegt werden. Wer die Optionen auf dem Weg über diese Transaktion ändert, ändert unmittelbar die implementierende Klasse des Interfaces ZIF_FG_CHART_OPTIONS.

Wenn man dieses spezielle Include so anschaut, hat man nicht den Eindruck, dass es sich um ABAP handelt. Dennoch ist es vollgültiges ABAP:

* Konfiguration des Charts "Gebots-Historie"

axis x.
dimension zeit.

axis y.
dimension wert.

colors :
blue,
#dfc329,
#a0df2a,
#e75fb8,
#df2a45,
#3333ff,
#95a2ef,
#df692a,
#5cb800,
#5fe7d3,
#5f74e7.

decimals : 3.

dot-style :
type star,
dot_size 5.

line-style :
type line,
width 4,
font-size 10.

title :
font-size 20px,
colour blue,
font-family Verdana,
text-align center.

* Syntax für die Open Flash Chart Templates
* siehe http://teethgrinder.co.uk/open-flash-chart-2/tooltip.php
tooltip :
`#val# <br>#date:H:i:s#<br>#title#`.


Der Code liest sich eher wie CSS als wie ABAP. Er ist spezifisch für die Aufgabe, das Chart zu präsentieren. Wie ist es möglich, dass es sich um syntaktisch korrektes ABAP handelt? Ganz einfach: Die Anweisungen wie title, decimals, color usw. sind Macros, die jeweils mit wenigen Zeilen implementiert sind.

define axis.
assign zif_fg_chart_options~gs_&1_axis to <ls_axis>.
end-of-definition.

define dimension.
<ls_axis>-dimension = zcl_fg_chart_helper=>gc_dimension-&1.
end-of-definition.

define colors.
perform get_color using '&1' changing lv_color.
append lv_color to zif_fg_chart_options~gt_colors.
end-of-definition.

define tooltip.
zif_fg_chart_options~gv_tooltip_template = &1.
end-of-definition.


Der folgende Screenshot zeigt die Pflegetransaktion - im wesentlichen ein Text Edit Control. Es gibt einen Button für die Syntaxprüfung, und die Funktionstaste zum Speichern ist belegt.



Die Syntaxprüfung besteht einerseits aus der Standard-Syntaxprüfung von ABAP. Andererseits werden noch eigene, speziellere Prüfungen hinzugefügt:

method zif_dsl_rules~check.

data: lt_code type stringtab.

split is_rule-code at cl_abap_char_utilities=>cr_lf
into table lt_code.

* Generelle Syntaxprüfung des Rahmenprogramms mit geändertem Include
go_dsl->check_syntax( lt_code ).

* Eigene Prüfungen
check_dsl_syntax( lt_code ).

endmethod.


Bei den eigenen Prüfungen kann man Gebrauch vom ABAP-Sourcecode-Scanner machen, der mit dem Statement scan abap-source verfügbar ist. Er löst bereits die Doppelpunkt-Komma-Syntax auf und erzeugt eine Folge von Statements, die aus einzelnen Tokens aufgebaut ist.

method check_dsl_syntax.

data: lt_token type stoken_tab,
lt_stmnt type sstmnt_tab.

field-symbols: <ls_stmnt> type sstmnt.

* ABAP-Code scannen:
* Leerzeichen und Kommentare ignorieren
* Eine Folge von Statements einlesen
* Doppelpunkt/Komma-Syntax wird aufgelöst in mehrere Statements

scan abap-source it_code tokens into lt_token
statements into lt_stmnt.

loop at lt_stmnt assigning <ls_stmnt>.

* Mach es zu einer DSL : Menge der erlaubten Befehle einschränken
go_dsl->check_token_defined( iv_index = <ls_stmnt>-from
it_token = lt_token
it_list = gt_macros ).

* Befehle einzeln prüfen
check_statement( is_stmnt = <ls_stmnt>
it_token = lt_token ).

endloop.

endmethod.


Die Method check_token_defined() prüft hier, dass nur die definierten Macros als Statements verwendet werden können. Es ist daher bei Verwendung dieser Transaktion nicht möglich, beliebigen ABAP-Code in das Includeprogramm einzufügen. Der Anwender bleibt beschränkt auf die "Sprache", die ich mit meinen Macros definiert habe. Erst durch diese Einschränkung wird der Quelltext zu einer echten DSL. Denn eine DSL ist unter anderem dadurch gekennzeichnet, dass sie eben keine Turing-vollständige Sprache ist, sondern nur ein Satz von Anweisungen, die auf ein konkretes Problem passen. Eine DSL ist eher als Konfigurations- denn als Programmiersprache anzusehen.

In der Methode check_statement können dann noch pro Statement spezifische Prüfungen durchgeführt werden.

So kann ich z.B. eine Prüfung einbauen, ob mit dem Statement COLORS #xxyyzz. ein korrekter Farbwert eingegeben wurde:

method CHECK_STATEMENT.

field-symbols: <ls_first_token> type stoken.

read table it_token assigning <ls_first_token>
index is_stmnt-from.

case <ls_first_token>-str.
when 'COLORS'.
check_colors_stmnt( is_stmnt = is_stmnt
it_token = it_token ).
when ...
endcase.
endmethod.

In Method check_colors_stmnt kann das zweite Token aus der Tabelle it_token gelesen werden, die das Statement darstellt (dieses Statement muss aus genau zwei Tokens bestehen), und einer Prüfung auf Gültigkeit unterzogen werden.

Was ich hier vorgeführt habe, ist die Variante der domänenspezifischen Programmierung, die Martin Fowler eine interne DSL nennt: Die Konfigurationen werden gleich in der "Hostsprache" ABAP formuliert, aber ohne dass man dies wirklich merkt. Man hat den Eindruck, ein Konfigurationsfile zu bearbeiten: die Begriffe des DSL-Codes sind der Domäne entnommen, für die das Programm eine Lösung darstellt. Dennoch speichert man gültigen ABAP-Code, der sofort zur gewünschten Änderung des Systemverhaltens führt. Insbesondere gibt es in diesem Fall keinen Codegenerierungsschritt. Die Angaben können so, wie sie gemacht werden, direkt in den Quelltext übernommen werden. Ein separater Transformationsschritt ist für interne DSL's nicht notwendig.

Im Grunde bleibe ich damit bei dem Konzept, das ich in dem Blog Domänenspezifische Programmierung in ABAP umrissen habe. Es ist es wert, weiterverfolgt zu werden.

Es gibt viele Möglichkeiten, domänenspezifische Programmierung einzusetzen. Der Übergang zu den Themen Refaktorisierung und Clean Code ist nahtlos: Schon wenn ich mich beim Entwickeln bemühe, die Stepdown-Regel[2] zu befolgen und niedrigere von höheren Abstraktionsebenen zu trennen, bleibt auf der höheren Ebene DSL-artiger Code stehen: Methoden- und Variablennamen entstammen der Sprache des Problems - die ABAP-Implementierungsdetails verbergen sich hinter Methodenaufrufen.[3]

Statt der direkten Manipulation eines produktiven Codeteils - wie in obigem Beispiel - können auch Metadaten erzeugt werden, etwa in Form von Tabelleneinträgen, die vom produktiven Programm gelesen werden und sein Verhalten steuern. Die DSL kann auch verwendet werden, um den Code einer produktiven Klasse zu generieren - dann wäre es eine externe DSL.

Ein Beispiel für eine externe DSL ist der BSP-View-Codegenerator von SAP: Der Webentwickler formuliert den View in Form von HTML-Code und einigen BSP-Direktiven - das ist seine Domäne. Der Codegenerator erzeugt daraus eine ABAP-Klasse, die zur Laufzeit einen String in den HTTP-Body schreibt.


[1] Die Anwendung sollte nicht nur auf allen normalen Browsern, sondern auch auf dem Internet Explorer laufen, und zwar bis herunter zur Version 8.
[2] Robert C. Martin, Clean Code, mitp-Verlag, München u.a. 2009, S. 67.
[3] Genauer besteht die abstrakte Codeebene aus Methodenaufrufen, deren Namen der Problemdomäne entstammen, und aus aktuellen Konfigurationsparametern. Diese können in einer separaten Konfigurationsdatei in Form einer DSL notiert werden. Oder die oberste Codeebene stellt selbst die DSL dar, so dass sowohl Methodenaufrufe als auch aktuelle Parameterwerte auf einmal gepflegt werden können. Beides sind gangbare Wege. Welcher Weg konkret vorzuziehen ist, muss von Fall zu Fall entschieden werden.

Keine Kommentare :