Rheinwerk Computing < openbook > Rheinwerk Computing - Professionelle Bücher. Auch für Einsteiger.
Professionelle Bücher. Auch für Einsteiger.

Inhaltsverzeichnis
1 Einleitung
2 Die Basis der Objektorientierung
3 Die Prinzipien des objektorientierten Entwurfs
4 Die Struktur objektorientierter Software
5 Vererbung und Polymorphie
6 Persistenz
7 Abläufe in einem objektorientierten System
8 Module und Architektur
9 Aspekte und Objektorientierung
10 Objektorientierung am Beispiel: Eine Web-Applikation mit PHP 5 und Ajax
A Verwendete Programmiersprachen
B Literaturverzeichnis
Stichwort
Ihre Meinung?

Spacer
 <<   zurück
Objektorientierte Programmierung von Bernhard Lahres, Gregor Rayman
Das umfassende Handbuch
Buch: Objektorientierte Programmierung

Objektorientierte Programmierung
2., aktualisierte und erweiterte Auflage, geb.
656 S., 49,90 Euro
Rheinwerk Computing
ISBN 978-3-8362-1401-8
Pfeil 9 Aspekte und Objektorientierung
  Pfeil 9.1 Trennung der Anliegen
    Pfeil 9.1.1 Kapselung von Daten
    Pfeil 9.1.2 Lösungsansätze zur Trennung von Anliegen
  Pfeil 9.2 Aspektorientiertes Programmieren
    Pfeil 9.2.1 Integration von aspektorientierten Verfahren in Frameworks
    Pfeil 9.2.2 Bestandteile der Aspekte
    Pfeil 9.2.3 Dynamisches Crosscutting
    Pfeil 9.2.4 Statisches Crosscutting
  Pfeil 9.3 Anwendungen der Aspektorientierung
    Pfeil 9.3.1 Zusätzliche Überprüfungen während der Übersetzung
    Pfeil 9.3.2 Logging
    Pfeil 9.3.3 Transaktionen und Profiling
    Pfeil 9.3.4 Design by Contract
    Pfeil 9.3.5 Introductions
    Pfeil 9.3.6 Aspektorientierter Observer
  Pfeil 9.4 Annotations
    Pfeil 9.4.1 Zusatzinformation zur Struktur eines Programms
    Pfeil 9.4.2 Annotations im Einsatz in Java und C#
    Pfeil 9.4.3 Beispiele für den Einsatz von Annotations


Rheinwerk Computing - Zum Seitenanfang

9.3 Anwendungen der Aspektorientierung  Zur nächsten ÜberschriftZur vorigen Überschrift

In den folgenden Abschnitten werden wir einige Probleme vorstellen, die sich durch die aspektorientierte Vorgehensweise elegant angehen lassen. Sie werden dabei eine ganze Reihe von Beispielen für Aspekte kennen lernen.


Rheinwerk Computing - Zum Seitenanfang

9.3.1 Zusätzliche Überprüfungen während der Übersetzung  Zur nächsten ÜberschriftZur vorigen Überschrift

Ein Compiler überprüft unsere Programme auf syntaktische Korrektheit. Für eine konkrete Anwendung können jedoch weitergehende Bedingungen gelten, die wir ebenfalls zur Laufzeit überprüfen möchten.

Nehmen Sie an, Sie setzen eine Anwendung mit Datenbankzugriff um. Die Vorstellung dürfte nicht schwer fallen, gilt diese Annahme doch für den überwiegenden Teil von Anwendungen. In Java sind die grundlegenden Methoden für den Zugriff auf relationale Datenbanken in den Paketen java.sql und javax.sql definiert. Diese können in allen anderen Paketen verwendet werden, die java.sql oder javax.sql importieren.

Nehmen Sie nun aber an, dass Sie in Ihrer Anwendung solche Datenbankzugriffe in einem dafür vorgesehenen Paket kapseln wollen. So soll es zum Beispiel verboten sein, Zugriffe auf die Datenbank direkt aus Paketen vorzunehmen, die dem Darstellungsbereich zugeordnet sind. Unser Ziel ist es dabei, die Persistenzbehandlung von anderen Teilen der Anwendung klar zu trennen. Dies geben Sie als Konvention an Ihr Entwicklungsteam und erklären in einem Treffen aller Beteiligten noch einmal, wie wichtig die Einhaltung dieser Konvention ist.

Absicherung gegen Programmierfehler

Sie wissen aber schon: Irren ist menschlich, und es wird nicht lange dauern, bis sich doch die ersten Aufrufe von Datenbankzugriffen in den Darstellungsklassen finden. Deswegen wollen Sie die Überprüfung der verbotenen Aufrufe automatisieren. Aspektorientierte Mechanismen können Ihnen dabei helfen. Am Beispiel von AspectJ stellen wir eine Möglichkeit vor, wie Sie Ihre eigenen Überprüfungen mit einbringen können.

Mit der folgenden Deklaration können Sie jeden Aufruf einer Operation aus den Paketen java.sql und javax.sql und allen ihren Unterpaketen aus dem Paket my.view und allen seinen Unterpaketen zu einem Fehler zu machen:

declare error: 
  (call(java.sql..* *.*(..)) || call(javax.sql..* *.*(..))) 
    && within(my.view..*): 
      "Don't call SQL from the View packages.";

Dadurch wird festgelegt, dass alle Aufrufe von Methoden aus den beiden SQL-Paketen, die innerhalb von Methoden aus dem Paket my.view oder einem Unterpaket getätigt werden, zu einer Fehlermeldung führen sollen.

Warnung bei verbotenen Aufrufen

Wenn Ihre Anwendung bereits existiert und Sie erst später feststellen, dass sich einige SQL-Aufrufe in die falschen Pakete eingeschlichen haben, kann es sinnvoll sein, dass Sie solche Aufrufe schnell entdecken, sie aber nicht als einen Fehler betrachten. Der Compiler soll nur eine Warnung ausgeben, damit Sie schnell die Stellen finden, die Sie überarbeiten müssen. Die folgende Deklaration warnt Sie bei allen SQL-Aufrufen, die innerhalb Ihrer Quelltexte (Paket my) liegen, aber außerhalb des Paketes my.db zu finden sind:

declare warning: 
  (call(java.sql..* *.*(..)) || call(javax.sql..* *.*(..))) 
    && within(my..*) && !within(my.db..*): 
      "All SQL-calls should be in the package my.db";

Rheinwerk Computing - Zum Seitenanfang

9.3.2 Logging  Zur nächsten ÜberschriftZur vorigen Überschrift

Ein anderes übergreifendes Anliegen ist die Protokollierung der Abläufe in einem Programm. Nehmen wir an, Sie möchten während der Entwicklung die Ausführung jeder öffentlichen Methode aller Klassen protokollieren. Die Objektorientierung bietet Ihnen die Möglichkeit, die Art der Protokollierung von den Methoden zu entkoppeln – die aufrufenden Methoden werden ausschließlich eine abstrakte Schnittstelle aufrufen. Wie sie implementiert ist, ob sie die Protokolleinträge in eine Datei, auf dem Bildschirm oder in eine Datenbank schreibt, interessiert sie nicht. Doch die Aufrufe der Protokollierung müssen Sie trotzdem in die Quelltexte der Methoden schreiben. Mit den Mitteln der Objektorientierung können Sie das Anliegen der Protokollierung nicht ganz von den Quelltexten der Methoden fernhalten.

Protokolleinträge vor und nach Methoden

Die Aspektorientierung ist hier dagegen eine große Hilfe. Mit dem folgenden Aspekt legen Sie fest, dass alle Ihre Klassen so modifiziert werden, dass sie vor und nach der Ausführung jeder öffentlichen Methode den entsprechenden Protokolleintrag vornehmen. In unserem Beispiel werden die Protokollausgaben auf der Konsole ausgegeben, es spricht aber nichts dagegen, auch hier eine Abstraktion zu verwenden.

public aspect Logging { 
  private int depth = 0; 
 
  private static String spaces(int n) 
//Wir verwenden die Methode spaces und die Variable depth, 
//um die Protokollausgabe optisch ansprechender geschachtelt zu gestalten. 
//Beachten Sie bitte, dass die Methode spaces privat ist. 
//Wäre sie selbst öffentlich, würde sie auch von dem Aspekt,
//so wie die Pointcuts definiert sind, betroffen. Das würde
//bei dem ersten Aufruf einer öffentlichen Methode zu einer
//Endlosschleife und letztendlich zu einem Stack-Überlauf führen.
] { 
    StringBuilder result = new StringBuilder(); 
    for (int i = 0; i < n; ++i) result.append(" "); 
    return result.toString(); 
  } 
 
  before(): execution (public * *(..)) { 
    System.out.println(spaces(depth) + "Before " + 
       thisJoinPointStaticPart.toLongString()); 
    ++depth; 
  } 
 
  after(): execution (public * *(..)) { 
    --depth; 
    System.out.println(spaces(depth) + "After " + 
      thisJoinPointStaticPart.toLongString()); 
  } 
}

Listing 9.3    Aspekt für Logging-Ausgaben

Sie haben nun festgelegt, dass vor (before) der Ausführung (execution) aller öffentlichen Methoden (public * * (..)) eine Beschreibung des aktuellen Joinpoints (also der aufgerufenen Methode) ausgegeben wird. Eine Einrückung erfolgt durch Leerzeichen, damit die Ausgabe übersichtlicher wird. Sie verwalten dazu die Variable depth, die vor jedem Methodenaufruf erhöht und nach jedem Methodenaufruf (after) wieder heruntergezählt wird.


Rheinwerk Computing - Zum Seitenanfang

9.3.3 Transaktionen und Profiling  Zur nächsten ÜberschriftZur vorigen Überschrift

Im vorherigen Beispiel haben wir den Aspektweber dazu veranlasst, alle öffentlichen Methoden um zwei Protokollausgaben zu erweitern. Dieser Eingriff war statisch. Bereits zur Übersetzungszeit war klar, an welchen Stellen des Programms die Protokollausgaben zu machen sind.

Eine ähnliche Aufgabe haben Sie vorliegen, wenn Sie die Zeit messen möchten, die das Programm bei der Ausführung der Methoden einer Klasse verbraucht. Hier müssen Sie vor jedem Aufruf einer gemessenen Methode die »Stoppuhr« starten und sie nach der Ausführung der Methode wieder stoppen.

Doch im Gegensatz zu unserem vorherigen Protokollierungsbeispiel können Sie nicht bereits zur Übersetzungszeit sagen, wann genau die Stoppuhr gestartet und wann sie gestoppt wird. Denn wenn eine gemessene Methode eine weitere gemessene Methode aufruft, darf die zweite Methode die Stoppuhr weder starten noch stoppen. Sonst würden Sie nicht die Gesamtzeit messen können. Bei einem statischen Pointcut könnten Sie sich mit einer Überprüfung einer Variablen helfen. In unserem Protokollierungsbeispiel verwenden wir die Variable depth, um die Tiefe der Schachtelung der Ausgabe zu steuern. Beim Messen der Zeit müssten Sie überprüfen, ob sie den Wert 0 hat, um nur dann die Stoppuhr zu starten oder zu stoppen.

Transaktionen über dynamische Pointcuts

Ein anderes Szenario, in dem Sie ein solches Verhalten brauchen, können Transaktionen sein. Sie können verlangen, dass bestimmte Methoden immer innerhalb einer Transaktion laufen. Die Transaktion sollte also vor dem Aufruf einer solchen Methode gestartet werden, wenn sie nicht bereits läuft, und nach dem Ende der Methode beendet werden, wenn sie beim Aufruf dieser Methode gestartet wurde.

AspectJ kann Ihnen diese Arbeit abnehmen und bietet Ihnen dafür die dynamischen Pointcuts an. Auch bei den dynamischen Pointcuts werden die Klassen statisch an den Stellen angepasst, an denen die Pointcut-Bedingung potenziell wahr werden kann, allerdings wird der dynamische Teil der Bedingung automatisch überprüft und der Advice nur dann ausgeführt, wenn die Bedingung zur Laufzeit wahr ist.

Schauen wir uns also ein Beispiel in AspectJ an. Die Klasse Test in Listing 9.4 hat zwei nicht statische öffentliche Methoden inner und outer. Sie wollen vor jedem Aufruf einer nicht statischen öffentlichen Methode der Klasse Test eine Stoppuhr starten und sie nach jedem solchen Aufruf stoppen. Aber nur dann, wenn sich der Aufruf nicht innerhalb eines anderen gemessenen Aufrufes befindet.

public class Test { 
 
   public void outer() { 
      System.out.println("Starting outer method"); 
      inner(); 
      System.out.println("Ending outer method"); 
   } 
 
   public void inner() { 
      System.out.println("In inner method"); 
   } 
 
   public static void main(String[] args) { 
     Test t = new Test(); 
     t.inner(); 
     t.outer(); 
   } 
}

Listing 9.4    Geschachtelte Methodenaufrufe

Der Messaspekt ist in Listing 9.5 aufgeführt.

public aspect TransactionalAspect { 
 
  private pointcut TestPointcut():     
      execution(public !static * Test.*(..)); 
 
  before(): TestPointcut()&&!cflowbelow(TestPointcut()) { 
    System.out.println("Starting timer"); 
  } 
 
   after(): TestPointcut() && !cflowbelow(TestPointcut()){ 
     System.out.println("Ending timer"); 
  } 
}

Listing 9.5    Aspekt zur Messung von Methodenlaufzeiten

!cflowbelow als dynamische Bedingung

Der statische TestPointcut in Zeile erfasst die Ausführung jeder öffentlichen nicht statischen Methode der Klasse Test. Durch die Klausel !cflowbelow(TestPointcut()) in den mit markierten Zeilen erweitern Sie die Pointcut-Bedingung der Advices um die dynamische Bedingung, dass sie nicht durchgeführt werden sollte, wenn sie sich innerhalb der Durchführung eines Joinpoints befindet, der selbst von TestPointcut erfasst wird.

Das Programm (die Methode main der Klasse Test) produziert erwartungsgemäß folgende Ausgabe:

Starting timer 
In inner method 
Ending timer 
Starting timer 
Starting outer method 
In inner method 
Ending outer method 
Ending timer

Wie Sie sehen, wird der Timer beim zweiten Aufruf der Methode inner nicht angefasst.


Rheinwerk Computing - Zum Seitenanfang

9.3.4 Design by Contract  Zur nächsten ÜberschriftZur vorigen Überschrift

Wie wir in Abschnitt 7.5.2, »Übernahme von Verantwortung: Unterklassen in der Pflicht«, über die Verträge zwischen den Aufrufern und den Bereitstellern einer Schnittstelle beschrieben haben, ist die Überprüfung von Vorbedingungen für eine Operation in den meisten Programmiersprachen problematisch. Findet die Überprüfung beim Aufrufer statt, muss sie redundant an allen Aufrufstellen stattfinden. Findet sie dagegen beim Aufgerufenen statt, muss sie möglicherweise redundant bei jeder Implementierung der Operation programmiert werden. Außerdem wird dann die Einhaltung des Kontrakts nur abhängig von den konkreten Testdaten überprüft werden, die Prüfung ist also unvollständig.

Salatöl, Diesel und explodierende LKWs

In Abschnitt 7.5.2 haben wir das Beispiel von auf Salatöl umgerüsteten Dieselfahrzeugen beschrieben. Wir haben festgestellt, dass Sie beim Prüfen der Vorbedingungen beim Betanken auf ein Dilemma stoßen. Wenn Sie die Prüfung dem aufgerufenen Modul (in unserem Beispiel den konkreten Fahrzeugen) überlassen, werden Sie eine inkorrekte Verwendung unserer Schnittstelle möglicherweise nur zufällig und spät herausfinden. Wenn Sie die Prüfungen an die Aufrufstellen verlagern, müssten Sie diese über unseren Code verstreuen, da es wesentlich mehr Aufrufstellen gibt als Methodenimplementierungen.

Aber wie bereits bei der Vorstellung unseres Beispiels versprochen, zeigen wir hier den Ausweg aus diesem Dilemma über aspektorientierte Vorgehensweisen. Leider musste unsere Salatöl-Tankstelle geschlossen werden, nachdem das Betanken eines Dieselfahrzeugs den ganzen Betrieb zum Stillstand gebracht und ein wütender LKW-Fahrer die Zapfsäulen demoliert hatte. Deshalb wählen wir hier ein etwas ungefährlicheres Szenario und zeigen die Überprüfung von Kontrakten am Beispiel eines Konverters, der eine Zeichenkette in eine Zahlenrepräsentation überführen soll.

Überprüfung von Verträgen anhand von Metadaten

Dabei lassen sich alle Stellen, an denen eine Überprüfung des Kontrakts stattfinden muss, programmatisch anhand der Programm-Metadaten bestimmen. Um redundanten Code an vielen programmatisch bestimmbaren Stellen eines Programms einzuweben, dafür sind die aspektorientierten Werkzeuge wie geschaffen.

Schauen wir uns also zunächst die Schnittstelle unseres Konverters an.

public interface Converter { 
  long convertNumber(String str); 
}

Diese Schnittstelle beschreibt eine Klasse, die Zeichenketten zu ganzen Zahlen konvertiert. Wir bestimmen, dass der Vertrag zwischen dem Aufrufer und dem Aufgerufenen die Vorbedingung enthält, dass der Aufrufparameter ausschließlich die Dezimalziffern enthält:

public aspect ConverterContract { 
  before(String str): 
   call(long Converter.convertNumber(String)) && args(str) { 
    for (char c: str.toCharArray()) { 
      if (c < '0' || c > '9') { 
        throw new IllegalArgumentException( 
          "Kann nur dezimale Ziffern enthalten"); 
      } 
    } 
  } 
}

Implementierung eines Konverters

Hier eine einfache Implementierung der Schnittstelle Converter. [Die Methode parseLong der Klasse long kann auch negative Nummern parsen. Unsere Vorbedingung ist hier strikter, da wir nur positive Nummern zulassen. ]

public class DecimalConverter implements Converter { 
  public long convertNumber(String str) { 
    return Long.parseLong(str); 
  } 
}

Doch außer der einfachen dezimalen Konversion können wir Zahlen auch aus anderen Notationen überführen. Die folgende Implementierung akzeptiert die Zahlen in den drei in Java üblichen Notationen: Fängt die Zahl mit 0x an, wird sie als hexadezimal verstanden, beginnt sie nur mit einer 0, handelt es sich um die oktale Notation, fängt sie mit einer anderen Ziffer an, geht es um die übliche dezimale Notation.

public class JavaNumberConverter implements Converter { 
  public long convertNumber(String str) { 
    if (str.startsWith("0x") || str.startsWith("0X")) 
      return Long.parseLong(str.substring(2), 16); 
    if (str.startsWith("0")) return Long.parseLong(str, 8); 
    return Long.parseLong(str); 
  } 
}

Die Klasse JavaNumberConverter implementiert die Schnittstelle Converter, sie muss also jeden Aufruf akzeptieren, der sich an die spezifizierte Bedingung hält, nur Dezimalziffern im Parameter zu übergeben.

Sie kann die Vorbedingung allerdings aufweichen. In unserem Falle tut sie das und akzeptiert auch die hexadezimale Notation mit dem Präfix 0x.

Wir müssen also den Pointcut in unserem Vertragsaspekt umformulieren:

before(String str): 
  call(long Converter.convertNumber(String)) && args(str) 
    && !call(long JavaNumberConverter.convertNumber(String))

Schauen wir uns jetzt die Aufrufstellen genauer an.

Converter con = new DecimalConverter(); 
System.out.println(con.convertNumber("123"));  
JavaNumberConverter jnc = new JavaNumberConverter(); 
con = jnc; 
System.out.println(jnc.convertNumber("0xc001babe")); 
System.out.println(con.convertNumber("0xc001babe"));

Prüfung von Vorbedingungen

In der Zeile wird die Operation convertNumber der Schnittstelle Converter aufgerufen. Die Vorbedingungsprüfung muss also stattfinden. Das Programm gibt hier 123 aus. In der Zeile wird die Methode convertNumber auf einer Variablen vom Typ JavaNumberConverter aufgerufen. JavaNumberConverter verlangt aber keine Überprüfung der Vorbedingung, daher gibt das Programm hier die Zahl 0xc001babe als 3221338814 aus.

Interessant wird es dann auf der Zeile . Die Variable con zeigt auf dasselbe Objekt wie die Variable jnc. Doch der Typ der Variablen con garantiert nur, dass sie auf einen Converter zeigt. Daher muss sich der Aufrufer an den Vertrag mit der Schnittstelle Converter halten. Aus diesem Grund wird in der Zeile eine IllegalArgumentException geworfen.

Dies ist genau das Verhalten, das wir erreichen wollten. Wir überprüfen an dieser Stelle also direkt die Möglichkeit, dass eine Kontraktverletzung auftreten könnte. Hätten wir dieses Vorgehen bei unserem Beispiel mit der Salatöl-Tankstelle gewählt, wäre direkt bei unserem ersten Testlauf mit salatölfähigen Autos aufgefallen, dass wir mit unserer Umsetzung auch normale Dieselautos mit Salatöl betanken können. Eine saubere Umsetzung von Design by Contract hätte eine gute Geschäftsidee gerettet und auf Jahre hinaus Arbeitsplätze in der Salatöl-Industrie gesichert.


Rheinwerk Computing - Zum Seitenanfang

9.3.5 Introductions  Zur nächsten ÜberschriftZur vorigen Überschrift

In den vorherigen Beispielen haben wir uns mit der Anpassung von Abläufen in einem Programm befasst. Wir haben in unsere Klassen Interzeptoren eingebunden, die das Verhalten der bereits vorhandenen Methoden geändert haben.

AspectJ und andere aspektorientierte Frameworks können die bestehende Klassenstruktur aber auch auf eine andere Art erweitern. Zum Beispiel können Sie in bestehende Klassen neue Elemente einfügen, sie bestimmte Schnittstellen implementieren lassen oder zwischen eine Klasse und ihre direkte Oberklasse eine weitere Klasse in die Vererbungshierarchie einfügen.

Erweiterung von Schnittstellen mit Methoden

Ein interessanter Anwendungsfall für die Erweiterung von bestehenden Klassen ist die Erweiterung der expliziten Schnittstellen in Java um konkrete Methoden. Dies bietet eine Alternative zur Mehrfachvererbung der Implementierung, die in Java nicht unterstützt wird. In folgendem Beispiel erweitern wir die Schnittstelle ReadableList<T> um die konkrete Methode last, die sich vollständig auf die abstrakten Methoden size() und get() der Schnittstelle abbilden lässt:

public interface ReadableList<T> { 
   public int size(); 
   public T get(int i); 
}

Eine Implementierung, die für die Datenhaltung ein Array verwendet:

public class SimpleList<T> implements ReadableList<T> { 
   private final T[] data; 
   public SimpleList(T... elements) { 
      data = elements.clone(); 
   } 
   public int size() { 
      return data.length; 
   } 
   public T get(int i) { 
      return data[i]; 
   } 
}

Neue Methode last für ReadableList

Der folgende Aspekt erweitert nun alle Implementierungen der Schnittstelle ReadableList um die Methode last().

public aspect ReadableListMixin { 
   public T ReadableList<T>.last() { 
      System.out.println("Mixed method last"); 
      return get(size()-1); 
   } 
}

Deswegen funktioniert folgender Aufruf und gibt die Zeichenkette "two" aus.

ReadableList r = new SimpleList<String>("one", "two"); 
System.out.println(r.last());

Über die aspektorientierte Spracherweiterung haben wir also die Möglichkeit erhalten, echte Mixins zusammen mit unseren Klassen zu verwenden.


Rheinwerk Computing - Zum Seitenanfang

9.3.6 Aspektorientierter Observer  topZur vorigen Überschrift

Mit den Mitteln der Aspektorientierung lassen sich auch einige Verfahren, die wir in objektorientierten Systemen häufiger finden, direkter ausdrücken.

Wir haben in Abschnitt 8.2.1 das Beobachter-Muster vorgestellt. Dabei registrieren sich Objekte bei anderen Objekten als Beobachter und werden im Fall von Änderungen benachrichtigt. Wir werden im Folgenden zeigen, wie wir dieses Muster mit Mitteln der Aspektorientierung umsetzen können.

Einfache hydrologische Regel

Im folgenden Beispiel betrachten wir die Klasse Well, die einen hypothetischen Brunnen repräsentiert. Dort wo solche Brunnen stehen, gelten sehr einfache hydrologische Regeln. Wenn es drei Tage hintereinander regnet, füllt sich der Brunnen mit genau einem Eimer Wasser, das man aus dem Brunnen abpumpen kann.

public class Well { 
  private int level; 
  public int getLevel() { 
    return level; 
  } 
  public void rain(int days) { 
    level += days / 3; 
  } 
  public void pump(int buckets) { 
    level -= Math.min(level, buckets); 
  } 
}

Listing 9.6    Modellierung eines Brunnens

Muster »Beobachter«

Die Klasse Well repräsentiert einen Brunnen und kann uns mit der Methode getLevel immer sagen, wie viel Wasser im Brunnen noch übrig geblieben ist. Wir möchten unsere Anwendung allerdings so erweitern, dass sie uns warnt, wenn der Pegel eines Brunnen zu tief sinkt. Um dies zu erreichen, möchten wir die Klasse Well so erweitern, dass sie eine Liste von Beobachtern verwaltet und bei jeder Änderung des Pegels die Beobachter benachrichtigt. Da die Klasse Well in anderen Anwendungen diese Funktionalität nicht braucht, möchten wir ihren Quelltext nicht ändern. Stattdessen verwenden wir AspectJ, um die nötigen Introductions und Interzeptoren in den Quelltext der Klasse Well einzuweben.

Beobachter-Klasse

Hier unsere Beobachter-Klasse. Jedes ihrer Exemplare kann genau einen Brunnen beobachten. Ein Brunnen kann aber von mehreren Beobachtern beobachtet werden.

public class WellObserver { 
  private Well well; 
  public WellObserver(Well well) { 
    this.well = well; 
  } 
  public void waterLevelChanged() { 
    System.out.println( 
     "New water level: " + well.getLevel()); 
  } 
}

Listing 9.7    Beobachter für einen Brunnen

Aspekt für Beobachter

Was wir jetzt noch brauchen, ist die Benachrichtigung der Beobachter, wenn sich der Pegel eines Brunnen ändert. Dafür sorgt der folgende Aspekt:

privileged public aspect WellObserverAspect { 
  private final transient Set<WellObserver> Well.observers 
    = new HashSet<WellObserver>();  
 
  after (WellObserver observer, Well well) returning:  
    execution (WellObserver.new(Well)) 
    && args(well) && target(observer) { 
    well.observers.add(observer); 
  } 
  after(Well well): set(* Well.level) && target(well) { 
    for (WellObserver observer: well.observers) { 
      observer.waterLevelChanged(); 
    } 
  }

Listing 9.8    Aspekt für die Benachrichtigung über Pegeländerungen

Der Aspekt ist als privilegiert deklariert und hat somit den Zugriff auf die privaten Elemente der Klasse Well. An der Stelle fügen wir jedem Exemplar der Klasse Well ein Attribut hinzu, das eine Menge (Set) repräsentiert. In diesem Set werden die Beobachter des Brunnens verwaltet. Damit haben wir also nachträglich die Klasse Well beobachtbar gemacht. In der Zeile erweitern wir den Konstruktor der Klasse WellObserver so, dass jeder Observer in die Menge der Observer bei dem beobachteten Brunnen eingetragen wird. Schließlich erzeugen wir in der Zeile einen Interzeptor, der dafür sorgt, dass nach jeder Änderung des Pegels (set(* Well.level)) alle Beobachter des Brunnens benachrichtigt werden.

Klasse Well bleibt unabhängig.

Die Klasse Well selbst hat also weiterhin »keine Ahnung« davon, dass jemand sie überhaupt beobachten kann. Welche ihrer Aktionen beobachtet werden und wie das geschieht, ist alleine Aufgabe des entsprechenden Aspekts.

Konstruieren wir nun einige aspektorientierte Brunnen und prüfen, ob die Beobachtung auch ohne direkte Mitarbeit der Klasse Well klappt.

Well wellA = new Well(); 
WellObserver observerA = new WellObserver(wellA); 
wellA.rain(15); 
wellA.pump(3); 
wellA.pump(3); 
wellA.pump(3);

Wir erhalten die folgende Ausgabe:

New water level: 5 
New water level: 2 
New water level: 0

Nachdem es 15 Tage geregnet hat, ändert sich der Wasserstand auf fünf Eimer. Dies wird korrekt beobachtet. Der erste Versuch, drei Eimer Wasser abzuschöpfen, führt dann zum Stand von zwei verbleibenden Eimern. Beim nächsten Schöpfversuch sinkt der Stand auf 0, was noch protokolliert wird. Beim letzten Versuch bleibt der Stand auf 0, es ist ja kein Wasser mehr zu holen.



Ihre Meinung

Wie hat Ihnen das Openbook gefallen? Wir freuen uns immer über Ihre Rückmeldung. Schreiben Sie uns gerne Ihr Feedback als E-Mail an kommunikation@rheinwerk-verlag.de.

 <<   zurück
  Zum Rheinwerk-Shop
Neuauflage: Objektorientierte Programmierung






Neuauflage:
Objektorientierte Programmierung

Jetzt Buch bestellen


 Ihre Meinung?
Wie hat Ihnen das Openbook gefallen?
Ihre Meinung

 Buchempfehlungen
Zum Rheinwerk-Shop: Java ist auch eine Insel






 Java ist auch
 eine Insel


Zum Rheinwerk-Shop: Schrödinger programmiert C++






 Schrödinger
 programmiert C++


Zum Rheinwerk-Shop: C++ Handbuch






 C++ Handbuch


Zum Rheinwerk-Shop: Einstieg in Python






 Einstieg in Python


Zum Rheinwerk-Shop: IT-Handbuch für Fachinformatiker






 IT-Handbuch für
 Fachinformatiker


 Lieferung
Versandkostenfrei bestellen in Deutschland, Österreich und der Schweiz
InfoInfo




Copyright © Rheinwerk Verlag GmbH 2009
Für Ihren privaten Gebrauch dürfen Sie die Online-Version natürlich ausdrucken. Ansonsten unterliegt das Openbook denselben Bestimmungen, wie die gebundene Ausgabe: Das Werk einschließlich aller seiner Teile ist urheberrechtlich geschützt.
Alle Rechte vorbehalten einschließlich der Vervielfältigung, Übersetzung, Mikroverfilmung sowie Einspeicherung und Verarbeitung in elektronischen Systemen.


Nutzungsbestimmungen | Datenschutz | Impressum

Rheinwerk Verlag GmbH, Rheinwerkallee 4, 53227 Bonn, Tel.: 0228.42150.0, Fax 0228.42150.77, service@rheinwerk-verlag.de

Cookie-Einstellungen ändern