Freitag, 5. August 2011

Dekorierte BSP-Elemente

Ein BSP-Element ist eine Softwareobjekt mit einer besonderen Aufrufkonvention: Es wird nicht direkt von ABAP-Codeeinheiten wie Methoden, Routinen oder Funktionsbausteinen aufgerufen, sondern als XML-Codefragment in einem View. In einem früheren Blog habe ich gezeigt, dass dies Auswirkungen auf die Unittests eines BSP-Elements hat: Der normale Aufrufrahmen für die Testklasse eines BSP-Elements enthält die Ausführung eines HTTP-Requests auf eine spezielle Testseite.

In diesem Blog will ich die Konsequenzen hinsichtlich der Erweiterbarkeit von BSP-Elementen nach dem Open-Closed-Prinzip von Bertrand Meyer betrachten:
Modules should be both open (for extension) and closed (for modification).
Lässt sich also ein bestehendes (z.B. in der BSP-Extension eines Drittlieferanten ausgeliefertes) BSP-Element erweitern, ohne das Element selbst modifizieren zu müssen?

Die Schnittstelle eines BSP-Elements besteht nicht aus Import- und Exportparametern, sondern aus Attributen und dem Elementinhalt. In Analogie zu den domänenspezifischen Sprachen (DSL), die ja nach Martin Fowler als eine API mit einer auf den Verwender angepassten Syntax aufgefasst werden sollten (er betrachtet DSL als "an alternative interface to a library than the usual command-query API" [1]), können wir auch ein BSP-Element als eine domänenspezifische Schnittstelle betrachten: Denn die BSP-Elemente fügen sich dank ihrer XML-Syntax nahtlos in den HTML-, XHTML- oder XML-Code ein, mit dem ein View üblicherweise formuliert wird.

Ein einfaches Beispiel: Um eine HTML-Tabelle mit den drei Spalten für Position, Material und Bezeichnung aus Modeldaten, etwa einer internen Tabelle gt_items in einem Model bestellung zu erzeugen, kann ich mit Vorteil das BSP-Element <z:table> verwenden, indem ich es im View auf folgende Weise aufrufe:
<z:table table_id="items" binding="//bestellung/gt_items">
<z:column name="posnr"
text="<=otr(z_bestellung/position>"
listPos="010"/>
<z:column name="matnr"
text="<=otr(z_bestellung/material>"
listPos="020"/>
<z:column name="arktx"
text="<=otr(z_bestellung/bezeichnung>"
listPos="030"/>
</z:table>

Das Element <z:table> aus der Tag Library Z meines BSP-Frameworks erzeugt bei diesem Aufruf eine HTML-Tabelle mit den angegebenen Spalten, von der Art dieses Beispiels.

Über das nackte Gerüst einer (mit Stylesheetklassen und ID's versehenen) HTML-Tabelle hinaus bietet die <z:table> eine Reihe von Features, die es selten nötig machen, das Element wirklich grundsätzlich erweitern zu müssen. Einige dieser Features habe ich in meiner Artikelserie zum Tabellentag erläutert.[2] Vor allem der Beitrag über die Exits des Tabellen-Tags zeigt, dass mit den eingebauten Zeitpunkten bereits viele Erweiterungen im Sinne des Open-Closed Principle möglich sind, d.h. ohne die Implementierung des <z:table>-Elements ändern zu müssen.

Hier will ich aber den Fall diskutieren, dass man sich die Logik des <z:table>-Elements in einem eigenen Tabellenelement zueigen machen möchte, sagen wir <zz:table>, indem man sie wiederverwendet - und damit den Teil des Elements, der aus der internen Tabelle die HTML-Elemente <table>, <tr>, <td> usw. generiert, nicht selbst implementieren muss.

Für gewöhnliche Klassen würde man in einem solchen Fall das Entwurfsmuster Dekorierer verwenden: Die neue Klasse enthält eine Referenz auf die bestehende Klasse in Form eines privaten Attributs und trägt (mindestens) die gleiche Schnittstelle wie die bestehende Klasse, indem sie z.B. ein Interface implementiert. Diese Implementierungen enthalten neben dem delegierenden Aufruf derselben Interfacemethode in der bestehenden Klasse noch den weiteren Code, um den sie durch die neue Klasse "angereichert" wird (daher der Name Dekorierer).

Nun steckt hinter einem BSP-Element ebenfalls eine Klasse: Seine Elementbehandlerklasse. Hier lässt sich der Dekorierer-Ansatz sofort übertragen. Die gemeinsame Schnittstelle ist das Interface if_bsp_element. Wir benötigen also zunächst einmal in der neuen Behandlerklasse – wenn das Element <zz:table> heisst, sollte sie zcl_bsp_zz_table heissen – eine Referenz auf eine Instanz der bestehenden Klasse zcl_bsp_z_table. Nennen wir diese Variable go_table.

Zum Zeitpunkt do_at_beginning (d.h. in der if_bsp_element-Methode dieses Namens) konstruieren wir das neue Objekt mit der dafür vorgesehenen Fabrikmethode:
method if_bsp_element~do_at_beginning.
* <z:table>-Element zum Erzeugen der Tabelle
call method zcl_bsp_z_table=>factory
exporting
binding = binding
...
zebra = zebra
receiving
element = go_table.
...
endmethod.

Bevor wir nun an das <z:table>-Element delegieren, müssen wir allerdings noch zwei weitere Dinge tun:

Erstens müssen wir den Viewkontext der BSP-Laufeit an das Delegationsobjekt übergeben:
* Seitenkontext an <z:table> übergeben
go_table->m_page_context = m_page_context.

Kaum ein BSP-Element, das wirklich etwas Nichttriviales macht, wird ohne diese Zuweisung richtig funktionieren. Die aktuellen Informationen zum Seitenkontext werden an zahlreichen Stellen in der Implementierung benötigt – nicht zuletzt braucht ein BSP-Element eine aktuelle writer-Instanz (die über den Seitenkontext zugänglich wird), um z.B. den generierten HTML-Code auszugeben.

Zweitens hängen wir das neu erzeugte BSP-Element, auch wenn es im View-Code nicht sichtbar ist, sondern verdeckt unter der Fassade des <zz:table>-Elements lebt, in den aktuellen Elementstack ein (das Casting auf die Context-Klasse ist hier nötig, weil die Operationen auf dem Elementstack bewusst nicht als Teil der Schnittstelle if_bsp_page_context entworfen wurden, sondern nur als öffentliche Methoden der implementierenden Klasse cl_bsp_page_context verfügbar sind):
  data: lo_page_context type ref to cl_bsp_page_context,
lo_delta0 type ref to if_bsp_delta_handler.

lo_page_context ?= m_page_context.
lo_page_context->element_push( element = go_table delta_handler = lo_delta0 ).

Das hat den Vorteil, dass innere Elemente, wie im obige Beispiel das Element <z:column>, mittels der Methode get_class_named_parent() genau wie im Standardfall mit dem übergeordneten <z:table>-Element kommunizieren können. Würden wir das Element nicht auf den Stack legen, so würden innere Elemente i.a. nicht mehr wie vorgesehen funktionieren.

Natürlich müssen wir das eingehängte Element nach Abarbeitung des erweiterten Elements auch wieder aus dem Elementstack entfernen. Das geschieht gegen Ende der Implementierung von do_at_end():
  data: lo_page_context type ref to cl_bsp_page_context,
lo_delta0 type ref to if_bsp_delta_handler.

lo_page_context ?= m_page_context.
lo_page_context->element_pop( element = go_table
delta_handler = lo_delta0 ).

Nun können wir wie beim Dekorierermuster verfahren und neben der Delegation neue Funktionalität hinzufügen. In der Regel wird das zu den Zeitpunkten do_at_beginning und do_at_end geschehen, es stehen aber natürlich auch alle anderen Methoden des Interface if_bsp_element für Erweiterungen zur Verfügung.

Ist dies gemacht, können wir das neue Element <zz:table> genauso wie das alte aufrufen,
aber auch zusätzliche Funktionen einfügen. Im folgenden Beispiel gibt es ein neues Attribut funkyAttribute und ein neues inneres Element <zz:funkyInnerElement>:
<zz:table table_id="items"
binding="//bestellung/gt_items"
funkyAttribute="dizzle">
<zz:funkyInnerElement foo="bar"/>
<z:column name="posnr"
text="<=otr(z_bestellung/position>"
listPos="010"/>
<z:column name="matnr"
text="<=otr(z_bestellung/material>"
listPos="010"/>
<z:column name="arktx"
text="<=otr(z_bestellung/bezeichnung>"
listPos="010"/>http://www.blogger.com/img/blank.gif
</z:table>


[1] Interview mit Martin Fowler und Rebecca Parsons über DSL in
Dr. Dobb's Journal (12/2010), http://drdobbs.com/architecture-and-design/228200852.
[2] Darstellung interner Tabellen mit CSS, Präsentation von Tabellen: die Datenquelle und Contexte und Tabellen-Exits.

Keine Kommentare :