Rekursive Aussichten

Kaum ist eine Softwareversion fertig, steht schon die nĂ€chste ‘Minor Release’ ins Haus. Dabei geht es nicht nur um Fehlerbeseitigung, auch die Anforderungen an ein Produkt Ă€ndern sich hĂ€ufig. Hier sind flexible Softwarearchitekturen gefragt. Ein Beispiel ist die aspektorientierte Programmierung, die die Wartbarkeit objektorientierter Software deutlich vereinfachen kann.

vorlesen Druckansicht 2 Kommentare lesen
Lesezeit: 12 Min.
Von
  • Krzysztof Czarnecki
  • Lutz Dominick
  • Ulrich W. Eisenecker
Inhaltsverzeichnis

Die beiden ersten Folgen dieses dreiteiligen C++-Kurses haben erlĂ€utert, wie sich bewĂ€hrte Programmiertechniken fĂŒr die aspektorientierte Programmierung (AOP) nutzen lassen. Gezeigt wurden andererseits Grenzen dieses Paradigmas bei der Behandlung von Ausnahmen. Dieser letzte Teil thematisiert die Verbindung von Aspekten mit freien Funktionen. Die kompletten Listings sind wie ĂŒblich ĂŒber den iX-Webserver verfĂŒgbar.

Soll einer Funktion nur ein bestimmter Aspekt hinzugefĂŒgt werden, scheint die Lösung verhĂ€ltnismĂ€ĂŸig einfach. Im Namensraum aspects wird eine Funktion gleichen Namens definiert. Diese fĂŒhrt zuerst den vorangehenden Aspektcode auf, ruft dann die Original-Funktion auf und fĂŒhrt schließlich den nachfolgenden Aspektcode aus. Importiert der Namensraum composed den Funktionsnamen aus dem Namensraum original, erhĂ€lt man die Original-Funktion ohne Aspektcode, importiert er ihn aus aspects, ist es die Funktion mit Aspektcode.

Aufwendiger gestaltet es sich, mehrere Aspekte in wechselnder Reihenfolge und unterschiedlicher Zahl mit einer Funktion zu verbinden (Listing 1). Der Kern der Lösung besteht darin, einen generischen Rahmen zu schaffen, in dem beliebig zusammengesetzter Aspektcode laufen kann und die Original-Funktion aufgerufen wird. Diese Aufgabe kommt dem Funktions-Template factorial zu (Listing 1, Zeile 78), dessen Template-Parameter der Typ einer durch Komposition gewonnenen Aspektklasse ist. Damit die Aspekte die Funktions- und RĂŒckgabeparameter inspizieren und modifizieren sowie auf das Auftreten einer Ausnahme reagieren können, werden alle diese GrĂ¶ĂŸen an ein Exemplar der synthetisierten Aspektklasse als Parameter ĂŒbergeben (Zeile 84). Das Ergebnis des Aufrufs der Original-Funktion steht in result (Zeile 87) und die Information ĂŒber das Auftreten einer Ausnahme in dem Flag exception (Zeile 92). Beide sind lokale Variablen einer AusprĂ€gung von factorial und stehen somit wĂ€hrend der gesamten Lebensdauer aller Aspekte zur VerfĂŒgung. Die drei Variablen n, result und exception bilden dabei einen gemeinsamen Zustand aller Aspekte. Jeder Aspekt kann daher in der ihm eigenen Weise auf eine Ausnahme reagieren. Die Implementierung des Funktions-Templates factorial bleibt davon unberĂŒhrt.

Mehr Infos

Listing 1

Beliebige Aspektkomposition fĂŒr freie Funktionen

  1     class CheckingException: public exception
2 {
3 public:
4 CheckingException (const char* msg):msg_(msg)
5 {}
6 const char* what() const throw()
7 {
8 return msg_;
9 }
10 private:
11 const char* msg_;
12 };
13
14
15 namespace original
16 {
17 int factorial(int n)
18 {
19 if (n < 0)
20 throw CheckingException("can not compute factorial of negative integrals");
21 if (n > 0)
22 return n * factorial(n-1);
23 else return 1;
24 }
25 }
26
27 namespace aspects
28 {
29 struct NilAspect // class representing NIL aspect for Factorial
30 {
31 NilAspect(const int& n_,const int& r_,const bool& exception)
32 {}
33 };
34
35 template <class NextAspect = NilAspect>
36 struct Tracing
37 {
38 Tracing(const int& n_,const int& r_,const bool& exception_)
39 :n(n_),r(r_),exception(exception_),nextAspect(n_,r_,exception_)
40 {
41 // to prevent alteration all values are
42 // treated as const reference
43 cout << "before factorial (" << n << ')' << endl;
44 }
45 ~Tracing()
46 {
47 if (!exception)
48 {
49 cout << "after normal return of factorial " << n << "! = " << r << endl;
50 }
51 else
52 {
53 cout << "after exceptional return of factorial" << endl;
54 }
55 cout << "always after factorial" << endl;
56 }
57 const int& n;
58 const int& r;
59 const bool& exception;
60 NextAspect nextAspect;
61 };

62
63 template <class NextAspect = NilAspect>
64 struct NotNegative
65 {
66 NotNegative(const int& n_,const int& r_,const bool& exception_)
67 :n(const_cast<int&>(n_)),r(r_),exception(exception_),
68 nextAspect(n_,r_,exception_)
69 {
70 n = (n < 0) ? 0 : n;
71 }
72 int& n;
73 const int& r;
74 const bool& exception;
75 NextAspect nextAspect;
76 };
77
78 template <class Aspect>
79 int factorial(int n)
80 // aspects might want to modify n, thus n is passed by value
81 {
82 bool exception = false;
83 int result;
84 Aspect a(n,result,exception);
85 try
86 {
87 result = original::factorial(n);
88 return result;
89 }
90 catch(...)
91 {
92 exception = true;
93 throw;
94 }
95 }
96 }
97
98 namespace composed
99 {
100 int factorial(const int& n)
101 {
102 // Variant 1: No aspect composed with factorial
103 // return aspects::factorial<aspects::NilAspect>(n);
104
105 // Variant 2: Trace aspect composed with factorial
106 // return aspects::factorial<aspects::Tracing<> >(n);
107
108 // Variant 3: Safe aspect composed with factorial
109 // return aspects::factorial<aspects::NotNegative<> >(n);
110
111 // Variant 4: Safe and trace aspect composed with factorial;
112 // trace aspect first
113
114 return aspects::factorial<aspects::NotNegative<aspects::Tracing<> > >(n);
115
116 // Variant 5: Safe and trace aspect composed with factorial;
117 // safe aspect first
118 // return aspects::factorial<aspects::Tracing<aspects::NotNegative<> > >(n);
119 }
120 }
121
122

Um die Aspekte in beliebiger Reihenfolge zusammenzufĂŒgen, wird jeder einzelne, beispielsweise Tracing, als Klassen-Template realisiert, dessen Parameter der Typ des nĂ€chsten Aspekts ist. Dadurch lassen sich die Aspekte wie in einer einfach verketteten Liste miteinander verbinden. Die Klasse NilAspect (Zeile 29) sorgt fĂŒr den Abschluss dieser Liste. Wird kein nĂ€chster Aspekt als Template-Parameter angegeben, ist NilAspect Standardvorgabe (Zeile 35).

Bei der Implementierung der Aspektklassen ist zu bedenken, welche Parameter der Original-Funktion nur einsehbar und welche Ă€nderbar sein sollen. Das Beispiel verwaltet die Parameter in Form konstanter Referenzen. Im Kopf des Funktions-Templates factorial wird jedoch der Parameter n aufgrund der Übergabe als Wert kopiert (Zeile 79). Deswegen kann nach einem const_cast in der Initialisiererliste von NotNegative (Zeile 67) eine nichtkonstante Referenz auf n_ initialisiert werden, die nachfolgend die VerĂ€nderung des Werts des Übergabeparameters erlaubt.

Im Namensraum composed erfolgt die Komposition der gewĂŒnschten Aspekte innerhalb einer weiteren Definition von factorial in einer der Aspektkomposition fĂŒr Klassen Ă€hnlichen Weise (zum Beispiel Zeile 114).

Das Beispiel aus Listing 1 zeigt einen interessanten Effekt. Der Aspektcode wird grundsĂ€tzlich nur vor und nach dem ersten Aufruf der rekursiv implementierten Funktion factorial() ausgefĂŒhrt. Das erklĂ€rt sich dadurch, dass innerhalb von factorial die Original-Funktion gerufen wird und nicht die im Namensraum composed enthaltene, die mit Aspekten verbunden ist (Zeile 87). HĂ€tte man den Aspektcode direkt in den Quellcode der Originalfunktion ĂŒbernommen, wĂŒrde er jedesmal bei rekursiven oder indirekt rekursiven Aufrufen der Funktion ausgefĂŒhrt werden. Die fĂŒr AspectJ verwendete Terminologie unterscheidet hier zwischen ‘Empfang einer Nachricht’ (Reception) und ‘AusfĂŒhrung einer Methode’ beziehungsweise Funktion (Execution). Diese Unterscheidung ist sowohl fĂŒr Memberfunktionen - einschließlich Selbstaufrufen - als auch fĂŒr freie Funktionen relevant.

Die bisher beschriebenen Mechanismen erlauben keine Implementierung einer rekursiven AusfĂŒhrung von Aspektcode fĂŒr freie Funktionen und fĂŒr frĂŒh gebundene Memberfunktion. Der Compiler setzt eben an der Stelle eines rekursiven oder indirekt rekursiven Funktionsaufrufs oder frĂŒh gebundenen Memberfunktionsaufrufs die Adresse der betreffenden Funktion ein.

FĂŒr Memberfunktionen mit spĂ€ter Bindung existiert jedoch eine Technik, die es erlaubt, den rekursiven oder nicht-rekursiven Aufruf von Aspektcode zu kontrollieren. Listing 2 zeigt dies anhand einer ‘rein objektorientierten’ Umsetzung der factorial()-Funktion aus Listing 1. Die Lösung ist verhĂ€ltnismĂ€ĂŸig einfach. FĂŒr den nicht-rekursiven Aufruf des Aspektcodes wird die im ersten Teil [1] beschriebene Weiterleitungstechnik eingesetzt. Hierdurch ‘verliert’ die Memberfunktion factorial() sozusagen ihre spĂ€te Bindung (Listing 2, Zeile 45). FĂŒr den rekursiven Aufruf des Aspektcodes kommt wie bisher Vererbung zum Einsatz, wodurch factorial() im resultierenden Code seine spĂ€te Bindung behĂ€lt (Zeile 56).

Mehr Infos

Listing 2

Flexible AusfĂŒhrung von Aspektcode bei rekursiven Memberfunktionen mit spĂ€ter Bindung

1    namespace original 
2 {
3 class Utilities
4 {
5 public:
6 virtual int factorial(int n)
7 {
8 if (n > 0)
9 return n * factorial(n-1);
10 else return 1;
11 }
12 };
13 }
14
15 namespace aspects
16 {
17
18
19 class ReportTracing
// helper class for implementing Tracing aspect
20 {
21 public:
22 ReportTracing (const char* function):
function_(function)
23 {
24 cout << "Before " << function_ << endl;
25 }
26 ~ ReportTracing ()
27 {
28 cout << "After " << function_ << endl;
29 }
30 private:
31 const char* function_;
32
33 };
34
35
36
37 template <class Class>
38 class NonRecursiveTracing
39 {
40 Class c;
41 public:
42 int factorial(int n)
43 {
44 ReportTracing t("factorial");
45 return c.factorial(n);
46 }
47 };
48
49 template <class Base>
50 class RecursiveTracing: public Base
51 {
52 public:
53 int factorial(int n)
54 {
55 ReportTracing t("factorial");
56 return Base::factorial(n);
57 }
58 };
59
60 }
61
62 namespace composed
63 {
64
65 // Utilities without aspects
66 // typedef original::Utilities Utilities;
67 // Utilities with Tracing aspect,
68 // no recursive execution of aspect code
69 // typedef aspects::NonRecursiveTracing
70 // <original::Utilities> Utilities;
71 // Utilities with Tracing aspect,
72 // with recursive // execution of aspect code
73 typedef aspects::RecursiveTracing<original::
Utilities> Utilities;
74 }

Der erste Teil des Tutorials ist ausfĂŒhrlich auf die unterschiedlichen Arten von Code (fachlicher, Aspekt-und Clientcode) eingegangen, hat allerdings bisher nur den Fall betrachtet, dass fachlicher und Clientcode verschieden sind. Was wĂŒrde beispielsweise geschehen, wenn man einem IntStack und seinen Int-Elementen den Aspekt Tracing hinzufĂŒgen wĂŒrde? Da erst der Clientcode den Namensraum composed importiert, wĂŒrden zwar dort die Bezeichner IntStack fĂŒr IntStack ⊗ Tracing und Int fĂŒr Int ⊗ Tracing stehen, aber im fachlichen Code hĂ€tte Int lediglich die Bedeutung von Int. Bei jedem Übergang von Client- in Fachcode und zurĂŒck wĂŒrde eine Änderung von IntStack Tracing ⊗ nach Int und umgekehrt erfolgen. Die Frage ist, wie man fachlichen Code auch in der Rolle als Clientcode in der gewĂŒnschten Weise vollstĂ€ndig mit Aspekten verbinden kann.

Dies verdeutlicht ein letztes Beispiel. Die Klassen Boy und Girl sehen beide einen Zeiger auf ein Exemplar der jeweils anderen Klasse vor, nĂ€mlich myGirlfriend und myBoyfriend (Listing 3, Zeilen 79 und 58). Mit der Memberfunktion setFriend() kann der Zeiger jeweils auf ein geeignetes Exemplar einer Klasse gerichtet werden. Die Memberfunktion talk() bewirkt, dass fĂŒr den Zeiger auf den Freund die Memberfunktion listen() aufgerufen wird. Somit verwenden sich die beiden Klassen direkt gegenseitig, sind also in höchstem Maße gekoppelt.

Mehr Infos

Listing 3

Flexible Konfiguration von Aspekten mit Hilfe von NamensrÀumen

  1     // tracing.h
2
3 #include <iostream>
4 using namespace std;
5
6 #ifndef _tracing_h_
7 #define _tracing_h_
8
9 namespace aspects
10 {
11 template <class Base>
12 class Tracing: public Base
13 {
14 public:
15 Tracing()
16 {
17 cout << "Tracing: Object created" << endl;
18 }
19 ~Tracing()
20 {
21 cout << "Tracing: Object deleted" << endl;
22 }
23 template <class T>
24 void setFriend(T* t)
25 {
26 cout << "Tracing: setFriend()" << endl;
27 Base::setFriend(t);
28 }
29 void talk()
30 {
31 cout << "Tracing: talk()" << endl;
32 Base::talk();
33 }
34 void listen(const char* c)
35 {
36 cout << "Tracing: listen()" << endl;
37 Base::listen(c);
38 }
39 };
40 }
41 #endif _tracing_h_
42
43 // girl.h
44
45 #ifndef _girl_h_
46 #define _girl_h_
47
48 namespace original_Girl
49 {
50 class Girl
51 {
52 public:
53 Girl();
54 void setFriend(Boy* g);
55 void talk();
56 void listen(const char* msg);
57 private:
58 Boy* myBoyfriend;
59 };
60 }
61
62 #endif _girl_h_
63
64 // boy.h
65
66 #ifndef _boy_h_
67 #define _boy_h_
68
69 namespace original_Boy
70 {
71 class Boy
72 {
73 public:
74 Boy();
75 void setFriend(Girl* g);
76 void talk();
77 void listen(const char* msg);
78 private:
79 Girl* myGirlfriend;
80 };
81 }
82
83 #endif _boy_h_
84
85 // girl.cpp
86
87 #include <iostream>
88 using namespace std;
89
90 #include "composed.h"
91
92 namespace original_Girl
93 {
94 Girl::Girl():myBoyfriend(0)
95 {}
96 void Girl::setFriend(Boy* g)
97 {
98 myBoyfriend = g;
99 }
100 void Girl::talk()
101 {
102 if (myBoyfriend)
103 myBoyfriend->listen("message from girl-friend");
104 }
105 void Girl::listen(const char* msg)
106 {
107 cout << "girl listens to " << msg << endl;
108 }
109 }
110
111 // boy.cpp
112
113 #include <iostream>
114 using namespace std;
115
116 #include "composed.h"
117
118 namespace original_Boy
119 {
120 Boy::Boy():myGirlfriend(0)
121 {}
122 void Boy::setFriend(Girl* g)
123 {
124 myGirlfriend = g;
125 }
126 void Boy::talk()
127 {
128 if (myGirlfriend)
129 myGirlfriend->listen("message from boy-friend");
130 }
131 void Boy::listen(const char* msg)
132 {
133 cout << "boy listens to " << msg << endl;
134 }
135 }
136
137 // composed.h
138
139 // #include "boy_and_girl.h"
140 // #include "add_tracing_only_to_boy.h"
141 // #include "add_tracing_only_to_girl.h"
142 #include "add_tracing_to_boy_and_girl.h"
143
144 // boy_and_girl.h
145
146 namespace original_Girl
147 {
148 class Girl;
149 }
150 namespace original_Boy
151 {
152 typedef original_Girl::Girl Girl;
153 }
154 #include "boy.h"
155
156 namespace original_Boy
157 {
158 class Boy;
159 }
160 namespace original_Girl
161 {
162 typedef original_Boy::Boy Boy;
163 }
164 #include "girl.h"
165
166 namespace composed
167 {
168 typedef original_Boy::Boy Boy;
169 typedef original_Girl::Girl Girl;
170 }
171
172 // add_tracing_only_to_boy.h
173
174 #include "tracing.h"
175
176 namespace original_Girl
177 {
178 class Girl;
179 }
180 namespace original_Boy
181 {
182 typedef original_Girl::Girl Girl;
183 }
184 #include "boy.h"
185
186 namespace original_Boy
187 {
188 class Boy;
189 }
190 namespace original_Girl
191 {
192 typedef aspects::Tracing<original_Boy::Boy> Boy;
193 }
194 #include "girl.h"
195
196 namespace composed
197 {
198 typedef aspects::Tracing<original_Boy::Boy> Boy;
199 typedef original_Girl::Girl Girl;
200 }
201
202 // add_tracing_only_to_girl.h
203
204 #include "tracing.h"
205
206 namespace original_Girl
207 {
208 class Girl;
209 }
210 namespace original_Boy
211 {
212 typedef aspects::Tracing<original_Girl::Girl> Girl;
213 }
214 #include "boy.h"
215
216 namespace original_Boy
217 {
218 class Boy;
219 }
220 namespace original_Girl
221 {
222 typedef original_Boy::Boy Boy;
223 }
224 #include "girl.h"
225
226 namespace composed
227 {
228 typedef original_Boy::Boy Boy;
229 typedef aspects::Tracing<original_Girl::Girl> Girl;
230 }
231
232 // add_tracing_to_boy_and_girl.h
233
234 #include "tracing.h"
235
236 namespace original_Girl
237 {
238 class Girl;
239 }
240 namespace original_Boy
241 {
242 typedef aspects::Tracing<original_Girl::Girl> Girl;
243 }
244 #include "boy.h"
245
246 namespace original_Boy
247 {
248 class Boy;
249 }
250 namespace original_Girl
251 {
252 typedef aspects::Tracing<original_Boy::Boy> Boy;
253 }
254 #include "girl.h"
255
256 namespace composed
257 {
258 typedef aspects::Tracing<original_Boy::Boy> Boy;
259 typedef aspects::Tracing<original_Girl::Girl> Girl;
260 }
261
262 // test.cpp
263
264 #include <iostream>
265
266 using namespace std;
267
268 #include "composed.h"
269 using namespace composed;
270
271 int main()
272 {
273 Boy bernie;
274 Girl gertrud;
275 bernie.setFriend(&gertrud);
276 gertrud.setFriend(&bernie);
277 bernie.talk();
278 gertrud.talk();
279 return 0;
280 }
281
282

Eine so enge Kopplung erzwingt die Trennung zwischen der Definition der beteiligten Klassen und ihrer Implementierung. DarĂŒber hinaus ist zumindest eine schwebende Deklaration erforderlich. Definierte man beispielsweise die Klasse Girl zuerst, mĂŒsste man zuvor mittels class Boy; die Klasse Boy bekannt machen. Anschließend darf der Typ Boy als Zeiger und als Referenz dienen, jedoch ohne auf Memberfunktionen oder -attribute zugreifen zu können. Die Definition der Klasse Girl sieht beispielsweise folgendermaßen aus:

class Boy;
class Girl
{
public:
Girl();
void setFriend(Boy* g);
void talk();
void listen(const char* msg);
private:
Boy* myBoyfriend;
};

Es empfiehlt sich daher, fĂŒr jede Klasse eigene Definitions- (.h-Datei) und eigene Implementierungsdateien (.cpp-Datei) anzulegen. Damit ergeben sich insgesamt fĂŒnf Dateien: boy.h, girl.h, boy.cpp, girl.cpp und test.cpp mit der Funktion main(), die Exemplare von Boy und Girl anlegt und deren Verwendung testet.

Der nÀchste Schritt besteht darin, eine möglichst einfache und generische Implementierung etwa des Aspekts Tracing zu erstellen, die die Erzeugung und Zerstörung von Exemplaren sowie die Aufrufe von setFriend(), talk() und listen() protokolliert.

Hierzu ist der Code so zu umzuschreiben, dass in girl.h, girl.cpp und test.cpp anstelle von Boy ĂŒberall Tracing<Boy> verwendet wird, und anstelle von Girl in boy.h, boy.cpp und test.cpp stets Tracing<Girl>.

Anschließend möchte man erreichen, dass alle nötigen Änderungen aus den betroffenen Dateien herausgezogen und in einer einzigen Datei, nĂ€mlich composed.h, lokal zusammengefasst werden. Die Dateien von Listing 3 zeigen, wie dies geht.

Da Boy innerhalb seiner Definition und der Implementierung wirklich Boy bedeuten muss, andererseits aber in der Definition und Verwendung der Klasse Girl sowie in test.cpp Boy oder Tracing<Boy> sein kann, folgt daraus zwingend, dass die Klassen Boy und Girl nicht lĂ€nger im gleichen Namensraum stehen können. Jede Klasse erhĂ€lt daher ihren eigenen Namensraum, dessen Bezeichner sich aus dem PrĂ€fix original_ gefolgt von dem Namen der in ihm enthaltenen Klasse zusammensetzt, also original_Girl und original_Boy. Nun kann etwa innerhalb des Namensraums original_Girl der Bezeichner Boy entweder als Boy (also ohne Aspekt) oder als Tracing<Boy> eingefĂŒhrt werden.

In ĂŒbereinstimmender Weise mĂŒssen die Bezeichner innerhalb des Namensraums composed definiert werden. Die Datei composed.h importiert zudem alle benötigen Dateien mit Aspektdefinitionen und -implementierungen. Anschließend ist nur noch darauf zu achten, dass alle Implementierungsdateien composed.h anstelle der ursprĂŒnglichen Definitionsdaten einbinden. In composed.h können nun alle möglichen Konfigurationen beschrieben werden: beide Klassen ohne Tracing, nur Boy ⊗ Tracing, nur Girl
⊗ Tracing oder beide Klassen mit Tracing. Der besseren Übersicht wegen sind die unterschiedlichen Konfigurationen in eigene Dateien ausgelagert, die ihrerseits in composed.h per Include-Anweisung eingebunden werden.

Will man vorhandenen Code an diese Form der aspektorientierten Programmierung anpassen, dĂŒrfte das meist einen gewissen Aufwand erfordern: Definition und Implementierung jeder Klasse und jeder Funktion erfolgen zwingend in einer separaten Datei innerhalb eines eigenen Namensraums. Auch die Erstellung der Konfiguration composed.h erfordert einige Disziplin. Nachteilig ist zudem, dass mit der Angabe des Namensraums vollstĂ€ndig qualifizierte Bezeichner, also etwa original_Girl::Girl, grundsĂ€tzlich den Zugriff auf den Bezeichner ohne Aspekt erlauben und somit nicht verwendet werden dĂŒrfen. Damit lĂ€sst sich die eingangs aufgestellte Forderung nach minimalen Eingriffen in existierenden Code nicht im gewĂŒnschten Umfang erfĂŒllen. Andererseits mag der Aufwand akzeptabel erscheinen, wenn die dadurch gewonnen Möglichkeiten aspektorientierter Programmierung entscheidend sind. Bei von vornherein neu zu entwickelndem Code sollte es einfach sein, das beschriebene Schema von Anfang an mit minimalem Zusatzaufwand zu berĂŒcksichtigen.

Viele im Zusammenhang von C++ und AOP relevante Punkte haben wir (noch) nicht untersucht und ĂŒberprĂŒft, etwa die Komposition von Aspekten und Member-Templates, Funktions-Templates und partiell oder vollstĂ€ndig spezialisierten Templates. Da C++ das Überladen von Operatoren gestattet und Expression Templates [[#literatur 3]] eine mĂ€chtige Möglichkeit sind, um zur Übersetzungszeit Parse-BĂ€ume von AusdrĂŒcken zu erzeugen, zu analysieren und zu manipulieren, wĂ€re auch die Untersuchung der Komposition von Aspekten mit Sequenzen und AusdrĂŒcken lohnenswert.

Eine interessante Erkenntnis ist, dass als Implementierungstechnik fĂŒr die aspektorientierte Programmierung in C++ anstelle der Transformation von Quellcode oder der dynamischen Metaprogrammierung auch ausschließlich spracheigene Mittel, vornehmlich Typ-Komposition, Überladen von Funktionsnamen, Verwendung von Alias-Namen und Template-Programmierung, eingesetzt werden können. Angesichts der KomplexitĂ€t von C++, die auch die Entwicklung eines entsprechenden PrĂ€prozessors fĂŒr AOP erheblich erschwert, ist das erstaunlich.

Die tiefgehende BeschĂ€ftigung mit diesem Programmierparadigma wirft die Frage nach dem Umfang von AOP an sich auf. Welche Sprachmerkmale können sinnvoll mit Aspekten kombiniert werden? Welche Eigenschaften muss eine Sprache aufweisen, so dass allein mit Sprachmitteln Aspekte implementiert und ihre Komposition mit den primĂ€ren Sprachmerkmalen beschrieben werden kann? Analog zur Abgeschlossenheit einer Menge bezĂŒglich einer Operation könnte man dies als ‘Abgeschlossenheit einer Programmiersprache bezĂŒglich AOP’ bezeichnen. Wie lĂ€sst sich die Komposition von Aspekten beschreiben und durchfĂŒhren? Die BeschĂ€ftigung mit der letzten Frage zeigt viele BezĂŒge zwischen aspektorientierter und generativer Programmierung.

Wem die Aufbereitung vorhandener Quellen von Hand zu mĂŒhsam ist, wird dafĂŒr vielleicht ein entsprechendes Werkzeug entwickeln. Wir wĂŒrden uns freuen, wenn Sie Ihre Erfahrungen mit uns und anderen Lesern teilen und uns ĂŒber die Entwicklung relevanter Werkzeuge informieren wĂŒrden.

Dr.-Ing. Krzysztof Czarnecki
ist im Bereich Softwaretechnik der DaimlerChrysler-Forschung tÀtig.

Lutz Dominick
arbeitet in der Zentralabteilung Technik der Siemens AG auf dem Gebiet innovativer Softwareentwicklungstechniken.

Dr. Ulrich W. Eisenecker
ist Professor fĂŒr Informatik an der Fachhochschule Kaiserslautern, Standort ZweibrĂŒcken.

[1] Krzysztof Czarnecki, Lutz Dominick, Ulrich W. Eisenecker; Erste Aussichten; Aspektorientierte Programmierung in C++: Teil 1; iX 8/2001, S. 143 ff.

[2] Krzysztof Czarnecki, Lutz Dominick, Ulrich W. Eisenecker; Multiple Aussichten; Aspektorientierte Programmierung in C++: Teil 2; iX 9/2001, S. 142 ff.

[3] Krzysztof Czarnecki, Ulrich W. Eisenecker; Generative Programming. Methods, Tools, and Applications; Addison-Wesley, Reading 2000

Mehr Infos

iX-TRACT

  • Mit Hilfe von Funktions-Templates lassen sich mehrere Aspekte in wechselnder Reihenfolge und unterschiedlicher Anzahl mit einer Funktion verbinden.
  • Memberfunktionen mit spĂ€ter Bindung erlauben durch Weiterleitung den nicht-rekursiven Aufruf von Aspektcode und durch Vererbung den rekursiven.
  • Bei Überschneidungen von fachlichem und Client-Code empfiehlt es sich, Definitionen und Implementierungen von Klassen in getrennten Dateien vorzunehmen und diese lokal in einer Datei zusammenzufĂŒhren, die das Programm per Include-Anweisung einbindet.

(ka)