Donnerstag, 19. Juni 2008

Refactoring in ABAP

Jede Minute Zeit, die ich auf Refactoring verwende, ist ein Fortschritt in Sachen Wartbarkeit und Erweiterbarkeit. Refactoring ist cool, weil ich während des Entwickelns in kleinen, sicheren Schritten die Programmqualität verbessern kann. Darüber freut sich jeder, der als nächstes meine Programmquelltexte bearbeiten muss. Aber: Nicht alle der im exzellenten Standardwerk von Martin Fowler [1] aufgeführten Refactoringmassnahmen sind für ABAP geeignet. Dafür kommen andere, spezifische hinzu. Viele von Fowlers Massnahmen gehen davon aus, dass Objekte beliebig klein werden dürfen, leicht wie Schwanenflaum - wie ein String oder gar ein elementares Datenobjekt. Das mag für Sprachen wie C++ ein guter Ansatz sein. In ABAP jedoch hat man Respekt vor Klassen, sie kommen gewichtiger daher. Eine Klasse ist vergleichbar einer Funktionsgruppe oder einem ausführbaren Programm. Das ist schon etwas. Natürlich sollten Klassen Experten für etwas Wohlbestimmtes sein und Aufgaben delegieren, statt alles an sich zu reissen (um nicht zum "Blob" zu entarten, siehe das Blob-Antipattern in [2]). Aber Unterklassen zu bilden, nur um im Coding einer Methode ein Switch/Case nicht mehr stehen zu haben ("Typenschlüssel durch Unterklassen ersetzen ([1], S.227)"), wäre in ABAP Objects ein Unding.

Andere Massnahmen übertreiben in meinen Augen das Kapselungsprinzip. Es ist gut und richtig, die Interna einer Klasse vor ihren Verwendern zu verbergen. Aber man muss deswegen öffentliche Attribute nicht grundsätzlich ablehnen. Öffentliche Attribute können mit einer "Gebrauchsanweisung" versehen sein. Bei richtigem Gebrauch arbeitet die Klasse korrekt, bei Missbrauch eben nicht. Es gibt auch für die Verwendung von öffentlichen Methoden gewisse Spielregeln, z.B. einzuhaltende Aufrufreihenfolgen oder Konventionen über die Schnittstellenparameter. Natürlich könnte man derartige Konventionen, da eben Code aufgerufen und nicht nur ein Datenobjekt abgegriffen wird, im Falle von Methoden programmgesteuert absichern. Ich halte es aber für einen übertriebenen Anspruch, sämtliche öffentlichen Attribute einer Klasse in die private section zu verschieben und nur durch Setter- und Getter-Methoden zugänglich zu machen.

In ABAP Objects kommt das schöne Feature der "Read Only"-Attribute hinzu. Das sind öffentliche Attribute, auf die nur lesender Zugriff möglich ist. Wo immer also ein Attribut von aussen unter keinen Umständen manipuliert werden darf, setzt man im Class Builder das Read-Only-Häkchen. Und schon ist alles gut. Ich habe nämlich von den OO-Puristen noch kein wirklich überzeugendes Argument gehört, warum ein nur lesbares öffentliches Attribut böse ist. Richtig, man veröffentlicht mit einem globalen Attribut insbesondere auch dessen Datentyp, z.B. eine bestimmte Struktur. Das gilt aber genauso für Methodenparameter. Normalerweise ist es übertriebener Aufwand, einen separaten Datentyp für den Verwender zu definieren und die Daten, mit denen man wirklich arbeitet, auf solche öffentlichen Typen abzubilden. In gewissen Fällen mag das richtig sein, zum Beispiel bei BAPIs, weil man den Overhead für das Datenmapping als Preis für eine eingefrorene Schnittstelle nimmt (man beachte die Vorsichtsmassregeln zu published interfaces in [4], S. 318, um auch hier noch ein Minimum an zukünftiger Erweiterbarkeit sicherzustellen). Innerhalb ein und desselben Programms, das zur Laufzeit aus einem Netz von kollaborierenden, aufeinander abgestimmten Objekten besteht, ist eine solche Abbilderei dagegen ein törichter Luxus, den nur praxisferne Puristen verlangen können.

Ich finde da die Maxime von Larry Wall sehr schön pragmatisch: Ein gutes Programm schätzt es, "wenn man sein Wohnzimmer nicht betritt, aber halt weil man nicht eingeladen ist, und nicht, weil ein Gewehr an der Wand hängt." [3] Wie das CPAN zeigt, kommt man mit einer Sprache wie Perl, die mit dieser Maxime arbeitet, sehr weit. Überlassen wir die Diskussionen um Privates und Öffentliches, um strengste Kapselung oder nur 99.5%ige den Theoretikern. Es gibt in der Literatur Diskussionen über so spannende Fragen wie: ob man den Zugriff auf private (!) Attribute innerhalb der Klasse nur mittels Setter- und Gettermethoden ausführen dürfe. Das sind nicht gerade die Probleme, die wir in der Programmierpraxis haben. In der Praxis sehe ich das grösste Potential in Refaktorisierungen, die mit dem Verschieben von Code zu tun haben - und vor allem mit der damit zusammenhängenden flächendeckenden Einführung von Unit Tests.

Für die meisten Refaktorisierungen stehen "Klasse extrahieren" und "Methode extrahieren" (oder in ABAP beispielsweise: Funktionsbaustein in Methode extrahieren) am Anfang. Oft beginnt man, dieses Muster anzuwenden, nur um den Code übersichtlicher zu gestalten oder um gleichen oder fast gleichen Code nur einmal im System zu haben (don't repeat yourself). Wenn dann aber die Klasse übersichtlicher ist, bemerkt man weitere Möglichkeiten.

Schön daher, dass das Extrahieren und Verschieben von Methoden in der ABAP Workbench so einfach möglich ist. Zum Beispiel Verschieben: Eine Methode verschiebe ich in eine andere Klasse, indem ich sie zunächst mit dem im Contextmenü erscheinenden "Kopieren"-Feature in die Zielklasse kopiere. Danach trage ich eine private Instanzvariable für das Delegationsobjekt ein und erzeuge das Objekt im Constructor oder in der Initialisierungsmethode der Klasse. Nun kümmere ich mich um die Konsistenz der kopierten Methode. Wenn sie nicht auf globale Variablen oder klassenlokale Typen zurückgriff, sollte die Methode in der neuen Klasse sofort aktivierbar sein. Wenn es in der alten Klasse Unit Tests gab, die sich auf die Methode beziehen, müssen diese kopiert und ggf. angepasst werden, bis sie in der Zielklasse auf "OK" gehen. Ist dies geschehen, ersetze ich den - durch das Kopieren doppelt vorhanden - Code der Originalmethode durch den delegierenden Methodenaufruf in die Fremdklasse. Die Unit Tests der Originalklasse sollten nun alle "OK" sein. Wenn nicht, passe ich an. Danach kann ich mithilfe des Verwendungsnachweises die Aufrufer der Methode ermitteln: Zunächst die Aufrufe innerhalb der Originalklasse, danach die Aufrufe aus fremden Klassen. Die klasseninternen Aufrufe kann ich nun direkt an das neue Delegationsobjekt weiterreichen.

Höchst praktisch ist in diesem Zusammenhang der in den Class Builder integrierte Refactoring Assistent. Er ist ideal geeignet für die Refaktorisierungen wie Klasse extrahieren, Interface extrahieren und nimmt einem ein bisschen von der Handarbeit ab, die bei solchen Refaktorisierungen nötig ist.



In ABAP ergeben sich aufgrund der Besonderheiten der Sprache noch weitere, die von Fowler aufgezählten Fälle ergänzende Refaktorisierungsmuster:

  • Teilfunktion einer Klasse in einer lokalen Klasse kapseln

    Oft lässt sich in einer Klasse eine Gruppe von Teilaufgaben ausmachen, die quer durch die Methoden verwendet wird. Ein guter Kandidat für eine eigene Klasse. Andererseits ist diese Gruppe vielleicht zu speziell oder die Kopplung mit der Klasse zu stark, um sie gleich in Form einer Workbenchklasse zu veröffentlichen (vielleicht ändert sich diese Wertung bei einer späteren Refactoringrunde). Der Code gewinnt bereits an Lesbarkeit, wenn diese Funktionen in eine lokale Klasse umziehen und über eine private Referenzvariable angesprochen werden.


  • API- und Datenbankaufrufe für Unit Tests über lokale Klassen umleiten

    Das läuft unter dem Thema "Testbar machen". Es ist oft nicht möglich, die Beschaffung von Informationen aus anderen Programmen oder von der Datenbank von deren Verarbeitung zu trennen - obwohl dies für Komponententests sehr wünschenswert wäre. Methoden enthalten eben oft eine Sequenz von Selektionen, API-Aufrufen und methodeneigenem Verarbeitungscode. Wollte man alle benötigten externen Aufrufe in eine eigene Methode auslagern, würde oft die Effizienz des Programms leiden (auch wenn die Lesbarkeit gewänne, so dass das durchaus ein empfehlenswertes Vorgehen zum Testbarmachen einer Klasse ist). Wenn man aber die Beschaffung der Informationen genau an den Orten stehen lassen muss, wo sie sind, mitten im methodeneigenen Code, dann hilft die Faktorisierung über eine lokale Klasse. Statt den benötigten SAP-Funktionsbaustein oder Datenbank-Select direkt aufzurufen, leitet man den Aufruf über eine lokale Klasse um. Ich verwende hierzu zwei lokale Klassen lcl_api und lcl_db. Das hat daneben noch den Vorteil, dass man die Schnittstellen auf ein für die eigene Methode optimal passendes Format anpassen kann. Mehr als Schnittstellenmapping sollten diese lokalen Klassen aber möglichst nicht enthalten, sie sollten ansonsten "dumm" sein. Denn beim Modultest werden sie abgeklemmt: Statt mit lcl_api und lcl_db arbeitet der Testee mit Stubs, mit Subklassen lcl_api_test und lcl_db_test, die dem zu testenden Code Informationen über das Umfeld vorgaukeln.


  • Lokale Klasse globalisieren (ins Repository umziehen)

    Sobald man feststellt, dass die Funktionalität einer lokalen Klasse genau das wäre, was man an einem zweiten Ort braucht, sollte man mit der lokalen Klasse in die Workbench umziehen. Hierzu wählt man als Zwischenschritt am besten die Vererbung: Man legt zunächst eine neue Workbenchklasse an und kopiert mit der Zwischenablage die Public, Protected und Private Section der lokalen Klassendefinition in die neue Klasse. Wenn sich dabei zeigt, dass lokale Typdeklarationen fehlen, sind diese in der Klasse noch anzulegen. Nun muss die Implementierung der einzelnen Methoden in die Workbenchklasse übernommen werden, ggf. müssen dann weitere Typen übernommen werden, bis die Klasse schliesslich aktivierbar ist. Dann fügt man mit dem Zusatz "inheriting from" die Vererbung in der lokalen Klassendefinition hinzu und kann dann die lokalen Definitionen und Implementierungen entfernen.

    Oft ist es so, dass nur ein Teil der Methoden wirklich allgemein verwendbar ist, ein anderer Teil jedoch besser in der lokalen Klasse verbleibt. Das lässt sich alles einrichten, da man nun eine "Oben-Unten"-Unterscheidung hat und abstrahieren kann, was abstrahierbar ist.


  • Lokalen Datentyp globalisieren (ins Repository umziehen)

    Auch bei den Datentypen gibt es die Möglichkeit, sie gemäss ihrer übergreifenden Verwendbarkeit entweder programmlokal zu definieren oder im Data Dictionary zu veröffentlichen. Ersteres ist nur für sehr programmspezifische Datentypen vorzuziehen, deren Einsatz auf das eine Programm (die eine Klasse, die eine Funktionsgruppe) beschränkt bleibt. Da man sich in dieser Einschätzung vertun kann, gibt es die Refaktorisierung "Lokalen Datentyp globalisieren". Ein konsistenter Zwischenschritt vor der Eliminierung des lokalen Datentyps ist es, nach Einrichtung des globalen Typs zunächst die Deklaration zu einer Referenz auf den DDIC-Typ zu ändern (indem man den lokalen Typ in der Form types: my_type type ztype. deklariert, wenn ztype der neue DDIC-Typ ist). Danach kann man sich daran begeben, den Typ my_type vollständig zu entfernen.


  • Schnittstellentyp generalisieren

    Manche Methoden werden in ihrer Benutzung umständlich, weil sie einen zu speziellen Datentyp für ihre Parameter verlangen. Eine beliebte Designschwäche ist es z.B., ein importiertes Textfeld mit einem Datentyp fester Länge zu typisieren, z.B. text30. Die Methode kann dann nicht mit einem String oder einem kürzeren Textfeld aufgerufen werden – da die Schnittstellenprüfung dann einen Fehler bringt. Richtig ist hier, einen generischen zeichenartigen Typ wie csequence oder gar clike zu verwenden und ihn ggf. intern mittels move in das gewünschte Datenformat zu wandeln.


  • Schnittstellentyp spezialisieren

    In anderen Fällen ist eine zu generische Typisierung eines Schnittstellenparameters schädlich. Die Typisierung ist ja ein Service des Compilers, der uns hilft, Fehler in der Parameterübergabe schon früh, nämlich zur Compilezeit zu erkennen. In manchen Fällen wird type any angegeben, obwohl eine konkrete Typisierung helfen könnte, Laufzeitfehler zu verhindern. Wenn die Typisierung nun nachträglich hinzugefügt wird, sollte auch für alle Methodenverwender eine Syntaxprüfung gemacht werden. Wenn man nicht alle Verwender im Zugriff hat, z.B. weil es sich um an viele Kunden ausgelieferte Software handelt, kann man wenigstens zu Beginn der Methodenimplementierung eine Typprüfung machen. Am effizientesten ist hierbei die Zuweisung des untypisierten Parameters an ein typisiertes Feldsymbol mittels assign. Es genügt, dieses Statement an den Beginn der Implementierung zu setzen. So können Fehler der Parameterübergabe zwar nicht zur Designzeit, aber wenigstens unmittelbar nach Aufruf der Methode erkannt werden, was die Analyse erleichtert.


  • Einen Funktionsbaustein in eine andere Funktionsgruppe umhängen

    Diese Refaktorisierung kann man in ABAP sehr einfach ausführen: Man stellt die Objektliste der alten Funktionsgruppe ein, positioniert auf den umzuhängenden Funktionsbaustein und wählt mit der rechten Maustaste "Umhängen", worauf man die neue Funktionsgruppe angeben kann.




    Je nachdem wie stark der Baustein mit der Funktionsgruppe verwachsen war, sind danach noch gewisse Teilobjekte umuzuziehen: Unterprogramme, Datentypen, lokale Klassen. Diese müssen von Hand aus der alten Funktionsgruppe entfernt und in der neuen Funktionsgruppe angelegt werden. Für die Verwender des Funktionsbausteins sind keine Anpassungen nötig.


  • Einen Funktionsbaustein in eine Klasse umziehen.

    Hier muss zunächst der Code händisch in die neue Methode kopiert werden. Sobald er in der Methode lauffähig ist, wird der Code im Funktionsbaustein gelöscht und durch die Beschaffung der Objektinstanz und den Aufruf der neuen Methode ersetzt, so dass der Code nur einmal im System vorhanden ist. Je nach Verbreitung des Funktionsbausteins muss man entweder diese Verschalung stehen lassen und den Baustein im Kurztext und in Inline-Kommentaren als obsolet kennzeichnen, oder man kann bei allen Verwendern den Funktionsbaustein- durch den Methodenaufruf ersetzen und schliesslich den Funktionsbaustein löschen.


  • Funktionsgruppe in eine Singleton-Klasse verwandeln

    Eine etwas aufwendigere Refaktorisierung, die aus mehreren Teilschritten besteht:

    • Neue Klasse anlegen und als Singleton auslegen (geschützte Instanzerzeugung, statischer Instanz-Getter)

    • Funktionsbausteinschnittstellen in Methoden kopieren

    • Funktionsbausteinimplementierungen in Methodenimplementierung kopieren (hier entsteht temporär eine Redundanz)

      • Unterprogramme, sofern sie nicht extern aufgerufen werden, in private Methoden kopieren

      • Globale Daten der Funktionsgruppe in globale private Attribute der Klasse kopieren

      • Instanz der neuen Klasse im LOAD-OF-PROGRAM der Funktionsgruppe beschaffen und als globales Delegationsobjekt vorhalten


    • Funktionsbausteinimplementierung durch Methodenaufruf ersetzen (die Redundanz ist somit wieder beseitigt)

      Zu diesem Zeitpunkt stellt die Funktionsgruppe nur noch eine Verschalung der Klasse dar. Die Musik spielt schon längst in der Klasse. Schliesslich kann man noch

    • Alle Verwender der Funktionsgruppe anpassen, so dass sie direkt die Klasse aufrufen.

    • Wenn alle Verwender umgestellt werden können, kann schliesslich die Funktionsgruppe gelöscht werden.



  • Methode mit verschiedenen Signaturen anbieten

    Multiple Signaturen derselben Methode sind in ABAP Objects nicht erlaubt. Eine Methode wird anhand ihres Namens immer mit einer festen Signatur verknüpft. Unschön wäre es, alle alternativen Möglichkeiten von Aufrufparametern als optional zu kennzeichnen, selbst wenn zu Beginn der Methode eine Verprobung der Parameter erfolgt. Besser ist es, die Methode in eine private Methode umzuziehen, vielleicht mit einem vorangestellten Unterstrich im Namen; verschiedene öffentliche Methoden mit den jeweils möglichen Parametern rufen dann intern diese private Methode auf.


  • Einzigen Exportparameter einer Methode in Returning-Parameter umwandeln

    Die funktionale Notation y = f(x) ist angenehmer und lesbarer als der an die Funktionsbausteinsyntax angelehnte call method f exporting iv_x = x importing ev_y = y. Voraussetzung ist, dass es nur einen Rückgabewert gibt. Wenn es mehrere gibt, ist es möglicherweise sinnvoll, sie zu einer Struktur zusammenfassen. Da Returningparameter immer by value übergeben werden, sind gewisse Dinge wie Reflexion jedoch nicht möglich: Die Methode kann - anders als bei Exportparametern - bei Returningparametern keine Details über den Typ des übergebenen Aktualparameters erfahren.


  • Einfache Attribute zu Struktur zusammenfassen

    Wenn ein ganzes Set von Feldern durch verschiedene Methoden herumgereicht wird, wird der Code unnötig aufgebläht. Ein ganz grosses Plus von ABAP (und C/C++, Assembler u.v.a.m.) im Vergleich zu Java ist, dass es strukturierte Datenobjekte gibt. Der Zugriff auf eine Datenstruktur ist wesentlich performanter als die Verwendung von Datenhaltungsobjekten, wozu man in Java für Datenstrukturen gezwungen wird.


  • Ein strukturiertes Feld verschlanken

    Oft ist es nützlich, statt grosse Strukturen wiederzuverwenden, selbstdefinierte Datenstrukturen mit einer reduzierten Menge von Komponenten zu verwenden. Das reduziert die Abhängigkeiten des Codes. Schlanke Strukturen können in ABAP bequem mit der Anweisung move-corresponding oder sogar direkt von der Datenbank mit select * into corresponding fields of table lt_schlank... versorgt werden.


  • Zugriffsmethode eines Tabellentyps verbessern

    Obwohl es schon seit vielen Jahren die sortierten und Hashtabellen gibt, arbeiten sowohl SAP-Entwickler als auch Berater immer noch wie vor Jahrtausenden nur mit Standardtabellen. Zwar hat sich herumgesprochen, dass es besser ist, Tabellentypen im DDIC anzulegen. Wenn man sich aber die bestehenden Tabellentypen einmal ansieht, muss man enttäuscht feststellen, dass fast nur Standardtabellen angelegt und nicht einmal Keyfelder definiert werden - was selbst für Standardtabellen sinnvoll wäre und die Effizienz von Leseoperationen steigern kann. Darüberhinaus gibt es zu SAP-Standardstrukturen jeweils zig Duplikate von Tabellentypen, die aber alle gleich langweilig und somit redundant sind (Standardtabellen mit Standardschlüssel). Wenn man einmal das unglaubliche Potential der sortierten und Hashtabellen erkannt hat, wird man Tabellen in bestehendem Code einfach einen anderen Tabellentyp unterschieben. Welcher Tabellentyp jeweils der richtige ist, richtet sich nach der vorherrschenden Zugriffsart auf diese Tabelle. Die neue Typisierung kann durch eine lokale Typdeklaration erfolgen. Wenn man die Verwender des bisherigen Tabellentyps im Überblick hat, kann man auch den Typ selbst noch nachträglich ändern. Einige Zugriffe wie read table ... with key ... werden dann durch den Zugriff read table ... with table key ... ersetzt werden müssen. Ebenso kann man den häufig an jeder Mülltonne abgesetzten sort ersatzlos streichen.


  • Feld durch Referenz auf Feld ersetzen

    Wenn - wie es anzustreben ist - die Aufgaben des Systems auf ein Netz von zusammenarbeitenden Klassen verteilt sind, kann es vorkommen, dass es übergreifend verwendete Felder gibt, die bei Instanzbeschaffung vom einen Objekt ans andere übergeben werden. Das erzeugt eine Redundanz und damit ein schwierig zu analysierendes Auseinanderlaufen von Feldinhalten. Um das zu verhindern, wandelt man den Typ des Feldes in einen Referenztyp um und übergibt die Referenz statt des Inhalts. Wenn es nur ein create data für das Feld gibt - und das sollte man so vorsehen - ist dann sichergestellt, dass das Feld in allen involvierten Objekten stets den aktuellen Wert hat.


  • Konstanten in Type-Pool auslagern

    Type-Pools für Datentypen sind eine überflüssige Erfindung - eine schlampige Umgehung der expliziten Typdeklaration an den dafür vorgesehenen Orten. Sehr sinnvoll und nützlich sind Type-Pools aber für Konstanten. Denn die im Type-Pool deklarierten Konstanten sind im System wirklich nur einmal vorhanden, auch wenn sie von zig Programmen verwendet werden. Dagegen erzeugt man durch schlichtes Includieren von Konstantendeklarationen, wie es früher üblich war, diese Konstanten in jedem geladenen Programm neu.



[1] Martin Fowler, Refactoring oder: wie Sie das Design vorhandener Software verbessern, Addison-Wesley, München 2005.
[2] William J. Brown, Raphael C. Malveau, Hays W. McCormick III, Thomas J. Mowbray:AntiPatterns - Entwurfsfehler erkennen und vermeiden, mitp, Heidelberg 2007.
[3] Larry Wall: Programmieren mit Perl, o'Reilly, Köln 2002, S. 314.
[4] James Shore, Shane Walden: The Art of Agile Development, o'Reilly, Cambridge et al., 2007.

Kommentare :

David hat gesagt…

Hi Rüdiger,
ist ja jetzt schon drei Jahre her, dass du den Artikel geschrieben hast. Aber vielleicht klappts ja trotzdem.

Du schreibst davon, dass du die Datenbankaufrufe in lokale Klassen verlagerst, um sie für das Testen durch Stubs austauschen zu können.

Wie teilst du deiner eigentlichen Klasse mit, ob sie nun den Stub, oder die eigentliche lokale Klasse benutzen soll?

Machst du das manuell im Quellcode vor dem Test, oder hast du dir da eine coole Methode überlegt, wie das automatisch stattfinden kann?

Rüdiger Plantiko hat gesagt…

Hallo David,

ich habe keinen coolen Trick dafür, den dann keiner versteht...

Ich mache es so, wie Du es wahrscheinlich erwartet hast:

Ausgangslage: Die zu testende Klasse hat ein privates Attribut go_db vom Typ ref to lcl_db, wobei in lcl_db die Datenbankzugriffe programmiert sind. Im Konstruktor der zu testenden Klasse wird "normalerweise" eine Instanz von lcl_db in das Attribut go_db gestellt. "Normalerweise" heisst: im normalen, produktiven Einsatz der Klasse.

Beim Testen stelle ich in dieses Attribut go_db eine Instanz einer Subklasse lcl_db_stub hinein, die anstelle von Datenbankzugriffen z.B. auf internen Tabellen operiert.

Das mache ich in der dafür vorgesehenen Setup-Methode der Unittestklasse, die ja vor jedem Testfall durchlaufen wird. Die Setup-Methode ist die Methode, in der ich auch die Instanz des zu testenden Objekts erzeuge.

Wie bekomme ich die Instanz da hinein? Es gibt wohl drei Möglichkeiten, aber nur die erste ist wirklich gut:

1. Dependency Injection
Der Konstruktor der zu testenden Klasse bekommt eine optionalen Importparameter io_db. Wird dieser beim Aufruf versorgt, so wird sein Wert in go_db gesetzt. Wird er nicht mitgegeben, so wird im Konstruktor der zu testenden Klasse eine Instanz von lcl_db erzeugt.

2. Sich zum lokalen Freund machen.
Mit dem Konstrukt class zcl... definition local friends lcl_... erlaube ich der lokalen Testklasse, auf das private Attribut go_db zuzugreifen und den Stub dort hineinzustellen. Das ist eher schlecht. Denn im Konstruktor der zu testenden Klasse wird ja doch bereits ein Exemplar von lcl_db erzeugt - auch wenn es danach durch die Neuzuweisung von go_db der Garbage Collection anheimfällt. Wenn der Konstruktor von lcl_db z.B. bereits Datenbankzugriffe enthält, werden diese auch im Test ausgeführt. Das ist aber gerade das, was wir nicht wollen.

3. Das Attribut go_db wird öffentlich gemacht
und kann so von aussen nach Instanzbeschaffung noch auf den Stub umgesetzt werden. Nicht gut, weil obendrein auch noch ein Internum der Klasse veröffentlicht wird: Insbesondere muss lcl_db dann einen geeigneten öffentlichen Typ haben, lokale Klassendefinition allein reicht dann nicht. Noch ein Nachteil.

Rüdiger Plantiko hat gesagt…

Zur Option 1 ist noch zu ergänzen, dass der Importparameter io_db vom Typ ref to object deklariert werden muss, nicht etwa ref to lcl_db, da ja die Klasse lcl_db nach aussen nicht bekannt ist. Wird ein Objekt übergeben, so muss es mit dem Casting-Move übergeben werden - vereinfacht könnte der Code etwa so aussehen:

method constructor.
...
if io_db is bound.
go_db ?= io_db.
else.
create object go_db.
endif.
...
endmethod.

Rüdiger Plantiko hat gesagt…

Und noch eine Ergänzung.

Auf

http://wiki.sdn.sap.com/wiki/display/ABAP/ABAP+Unit+Best+Practices

habe ich einige praktische Tips zum Thema Modultests gesammelt. Ein Blick hinein könnte sich lohnen...

David hat gesagt…

Hey,
Danke für die schnelle Antwort. Dependency Injection ist auch für mich das Stichwort, wobei ich dabei den Weg gegangen bin, in einer Datenbanktabelle Interfaces und ihre Implementierungen zu hinterlegen. Durch den Einsatz einer extra Klasse, die sich um das Erstellen der konkreten Objekte kümmert, habe ich die Instanziierung völlig ausgelagert und kann diese dann dort beeinflussen
(z.B. Ein Flag setzen, dass die Testimplementierungen benutzt werden sollen)

Rüdiger Plantiko hat gesagt…

Aha, also eine Objektfabrik. Genau in der von Dir beschriebenen Form nutzen wir bei der Migros auch so etwas. Allerdings zu anderen Zwecken. Nicht um produktive DB-Klassen durch Teststubs zu ersetzen, sondern um nach Quertransport einer Klasse in ein anderes SAP-System mit Hilfe einer passenden Subklasse kleine Änderungen anbringen zu können.
(Das Verfahren habe ich auch im BSP-Praxisbuch beschrieben, Kapitel 3.5.5.3)

David hat gesagt…

Sehr gut. Das heißt, dass meine Überlegungen nicht völlig Praxisfern sind. Danke für deine Antworten