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.4 Annotations  Zur nächsten ÜberschriftZur vorigen Überschrift

In Abschnitt 9.1.2 haben wir uns mit den Metainformationen befasst, die in einem Programm von vornherein vorhanden sind – die Struktur der Klassen, ihre Namen, die Namen der Operationen und der Methoden, die Typen der Parametern und so weiter. Das sind auch die Metainformationen, die ein Compiler beziehungsweise der Interpreter einer Programmiersprache braucht.

Doch es gibt auch andere interessante Informationen über die Struktur des Programms, die für einen Compiler oder Interpreter irrelevant sind.


Rheinwerk Computing - Zum Seitenanfang

9.4.1 Zusatzinformation zur Struktur eines Programms  Zur nächsten ÜberschriftZur vorigen Überschrift

Für den Compiler ist es nicht wichtig, ob eine Methode setAmount oder asdfg heißt. Für den Compiler ist es egal, ob eine Methode testDivision oder purgeDatabase heißt. Eine Methode ist für ihn eine Routine, die einer Klasse zugeordnet ist.

Metainformation in Programmiersprachen

Manche Metainformationen sind für die Programme so wichtig und so relevant, dass sie durch Konstrukte der Programmiersprache selbst beschrieben werden können. So kann man in Java zum Beispiel mit dem Schlüsselwort transient bestimmen, dass bestimmte Felder nicht serialisiert werden dürfen, oder mit synchronized Methoden markieren, die für jedes Exemplar nicht in mehreren Threads gleichzeitig laufen können.

Doch für uns gibt es durchaus auch auf der Metaebene andere Unterschiede zwischen den Klassen und ihren Methoden. Wir möchten unterscheiden können, welche Eigenschaften einer GUI-Komponente in einem visuellen Editor dargestellt werden können, wir möchten, dass unser Testtool alle vorbereiteten Tests durchführt, nicht aber andere Methoden aufruft.

Namenskonventionen

Eine große Hilfe können hier Namenskonventionen sein. So bestimmt zum Beispiel die Spezifikation von Java Beans, dass jede Eigenschaft einer Bean, die gelesen werden kann, durch eine Methode repräsentiert wird, die mit get anfängt (oder mit is für boolesche Werte), und jede änderbare Eigenschaft durch eine Methode, die mit set anfängt. Das Testframework JUnit in seinen älteren Versionen geht davon aus, dass jede Testmethode mit test anfängt. Zusammen mit Möglichkeiten der Reflexion in einer Programmiersprache können die aus diesen Konventionen resultierenden Informationen dann auch zur Laufzeit eines Programms ausgewertet werden.

Doch durch Namenskonventionen können wir nicht alle benötigten Metainformationen den Metaprogrammen auf vernünftige Art bereitstellen. Wie soll man zum Beispiel eine Methode bezeichnen, die innerhalb einer Transaktion durchgeführt werden soll? Wie soll man spezifizieren, in welcher Tabelle Exemplare einer Klasse gespeichert werden?

Eine Hilfe bieten hier externe Konfigurationsdateien. Deren Einsatz ist vor allem dann sinnvoll, wenn die programmierten Klassen in verschiedenen Kontexten unterschiedlich konfiguriert werden.

Diskussion: Konvention oder Konfiguration

Bernhard: Konfigurationsdateien machen doch ein Programm nur komplexer. Ich habe dann einen weiteren Punkt außerhalb des Source-Codes, an dem möglicherweise redundante Information liegt. Ich würde statt Konfiguration lieber auf Namenskonventionen zurückgreifen und zum Beispiel die Exemplare einer Klasse in einer Tabelle speichern, die genauso heißt wie die Klasse.

Gregor: Das ist aber nicht so einfach. Vor allem dann nicht, wenn es um das Speichern einer ganzen Klassenhierarchie und der Beziehungen zwischen den Klassen geht. Wie man in Kapitel 6, »Persistenz«, sehen kann, gibt es verschiedene Möglichkeiten, wie man Klassen auf Tabellen abbilden kann.

Bernhard: Das stimmt. Aber trotzdem würde ich lieber eine einfache Konvention definieren und nur bei Abweichungen etwas konfigurieren. Denn jede Zeile in einer Konfigurationsdatei ist auch eine Zeile, in der ich einen Fehler machen kann.

Gregor: Das ist vernünftig. Man sollte immer eine Konvention der Notwendigkeit einer Konfiguration vorziehen. Aber manchmal geht es halt ohne Konfiguration nicht.

Doch auch wenn man durch die Konfigurationsdateien alle nötigen Metainformationen bereitstellen kann, in manchen Situationen sind sie unhandlich. Vor allem dann, wenn es sich um Zusatzinformationen handelt, die das Programm selbst beschreiben und nicht seine Einbindung in einen speziellen Kontext.

So kann es zum Beispiel durchaus unterschiedliche Konfigurationen der Abbildung der Klassenstruktur auf die Tabellenstruktur einer relationalen Datenbank geben, aber die Zusatzinformation, dass eine Methode immer in einem neuen Thread gestartet werden soll, bleibt für alle Installationen des Programms gleich. Diese Information ist am besten direkt im Quelltext der Methode aufgehoben, nicht in einer externen Konfigurationsdatei.

Zusatzinformation in Programmstruktur

Wir brauchen also eine Möglichkeit, solche Zusatzinformationen in die Struktur der Programme einzubinden. Die interpretierten Skriptsprachen wie JavaScript, Python oder Ruby bieten eine Reihe von Möglichkeiten. Konstrukte definieren.

Diese Möglichkeiten haben wir in den kompilierten Programmiersprachen unser Programm einbringen zu können.


Rheinwerk Computing - Zum Seitenanfang

9.4.2 Annotations im Einsatz in Java und C#  Zur nächsten ÜberschriftZur vorigen Überschrift

In C# oder Java ab der Version 5 erhalten wir diese explizite Unterstützung. Hier können wir zusätzliche Metainformation mit sogenannten Annotations (Anmerkungen) bereitstellen.


Icon Hinweis Annotations (Anmerkungen)

Annotations sind strukturierte Zusatzinformationen zu den Strukturelementen eines Programmes, die programmtechnisch zur Übersetzungszeit oder zur Laufzeit des Programms ausgewertet werden können.


Vordefinierte Annotations

In Java ab der Version 5 sind bereits einige vordefinierte Annotations verfügbar, die Hinweise für den Java-Compiler enthalten.

Mit der Annotation @Override werden Methoden markiert, die eine geerbte Methode überschreiben sollten. Sie signalisieren dem Compiler, dass er einen Fehler melden soll, wenn es sich um eine neue Methode handelt, wir also nicht wie eigentlich spezifiziert eine andere Methode überschreiben. Die Annotations selbst können Parameter haben.

Mit der Annotation @SuppressWarnings können wir zum Beispiel bestimmen, dass bestimmte Compilerwarnungen nicht ausgegeben werden sollen. Welche, das wird durch den Wert eines Parameters bestimmt. @SuppressWarnings("all") veranlasst zum Beispiel den Java Compiler in Eclipse, alle Warnungen für das annotierte Element zu unterdrücken.

Den Annotations selbst können wir wiederum andere Annotations hinzufügen. So können wir zum Beispiel bestimmen, für welche Elemente die Annotations gültig sind (Klassen, Pakete, Methoden, Felder, lokale Variablen, Annotations und so weiter) oder wo die Zusatzinformation sichtbar sein soll.

Annotations zur Übersetzungszeit

Es gibt Annotations, die der Compiler verwenden soll, die aber nicht in das übersetzte Programm einfließen sollen. @Override und @SuppressWarnings sind solche Annotations. Andere Annotations sollen zwar in das Kompilat einfließen, sie werden aber zur Laufzeit nicht gebraucht. Solche Annotations können von anderen Werkzeugen noch zur Übersetzungszeit verwendet werden, sie können solche Informationen beim Laden eines Programms nutzen.

Annotations zur Laufzeit

Schließlich gibt es natürlich auch Annotations, deren Zusatzinformationen wir zur Laufzeit eines Programms auswerten wollen. Die Information, in welcher Tabelle die Exemplare einer Klasse gespeichert werden, wenn es keinen Eintrag in einer Konfigurationsdatei gibt, können wir in einer solchen Annotation speichern. Selbstverständlich können wir die Informationen aus Annotations auch für die aspektorientierte Programmierung nutzen.


Rheinwerk Computing - Zum Seitenanfang

9.4.3 Beispiele für den Einsatz von Annotations  topZur vorigen Überschrift

Programmverhalten ändern

Betrachten wir nun ein Beispiel, bei dem wir über Annotations das Verhalten eines Programms modifizieren.

AWT

Das meistverwendete Framework, um in Java grafische Benutzerschnittstellen zu programmieren, ist Swing, das auf dem älteren AWT (Abstract Windowing Toolkit) basiert. [Eine Alternative bietet zum Beispiel das Framework SWT, auf dem das Eclipse-Framework basiert. ]

Die Swing-Elemente sind nicht threadsicher, wir können ihre Methoden zwar in verschiedenen Threads aufrufen, aber wir müssen dann selbst für die Synchronisierung der Zugriffe auf ihre Datenelemente sorgen. Die Synchronisierung ist am einfachsten, wenn alles in einem einzigen Thread läuft – dann gibt es nämlich nichts zum Synchronisieren. Dafür gibt es in Swing-Anwendungen auch bereits einen Thread, der dafür vorgesehen ist, nämlich den AWT-Ereignisthread. Aufrufe von Swing-Methoden, die durch Eingaben eines Benutzers angestoßen werden, laufen in diesem Thread ab.

Aufrufe von Methoden im Ereignisthread

Wenn wir nun selbst weitere Methoden schreiben, die mit Swing-Elementen arbeiten, müssen wir nun aber bei jedem Aufruf dafür sorgen, dass diese auch wirklich in diesem AWT-Ereignisthread gestartet werden. Tun wir das nicht und rufen die Methoden der Spring- und AWT-Elemente in unterschiedlichen Threads auf, kann es passieren, dass wir mit inkonsistenten Daten arbeiten.

Wir können dies am einfachsten vermeiden, wenn wir keine eigenen Threads starten und alle betroffenen Methoden im AWT-Ereignisthread ablaufen lassen. Doch wenn die Abarbeitung unserer Methoden lange dauert, entsteht so eine hässlich träge Benutzerschnittstelle, die viel zu langsam auf die Benutzereingaben reagiert. Daher ist es besser, wenn wir lange laufende Aufgaben in separaten Threads starten. Wenn diese die Darstellung von Swing-Elementen aktualisieren möchten, sorgen wir mit SwingUtilities.invokeLater dafür, dass die Swing-Methoden im AWT-Ereignisthread aufgerufen werden.

Nun ist es natürlich aufwändig und fehleranfällig, bei jedem einzelnen Aufruf einer Methode darauf zu achten, dass eine Methode auch wirklich im AWT-Ereignisthread ausgeführt wird. Deswegen erstellen wir uns eine aspektorientierte Erweiterung, welche die Ausführung entsprechend markierter Methoden automatisch in den AWT-Ereignisthread verschiebt.

Annotation EventThread

Dazu benötigen wir zunächst eine Annotation EventThread, mit der wir solche Methoden markieren werden.

@Retention(RetentionPolicy.CLASS) 
@Target(ElementType.METHOD) 
public @interface EventThread {}

Die Annotation soll nur für Methoden verwendet werden, daher ist sie mit der Annotation @Target(ElementType.METHOD) markiert, und sie sollte in dem übersetzten Code für den Aspektweber [Ein Aspektweber ist ein Teil eines aspektorientierten Compilers oder Frameworks, welches dafür verantwortlich ist, dass das übersetzte Programm den definierten Aspekten entsprechend an den richtigen Stellen – den Pointcuts – modifiziert wird. ] enthalten bleiben. Zur Laufzeit wird sie nicht mehr benötigt. Deswegen markieren wir sie mit der Annotation @Retention(RetentionPolicy.CLASS) [Die RetentionPolicy.CLASS ist der Standard für Annotations, daher brauchen wir diese Annotation nicht explizit anzugeben. ] .

Aspekt EventThreadAspect

Der folgende Aspekt sorgt dafür, dass die Ausführung jeder so markierten Methode in den AWT-Ereignisthread verschoben wird, wenn die Methode in einem anderen Thread aufgerufen wird. Der Aufruf proceed ruft die ursprüngliche Methode auf, wenn wir uns ohnehin bereits im AWT-Ereignisthread befinden.

public aspect EventThreadAspect { 
   void around(): @annotation(EventThread) 
     && execution(void *.*(..)) { 
      if (SwingUtilities.isEventDispatchThread()) { 
         proceed(); 
      } else { 
         SwingUtilities.invokeLater(new Runnable() { 
            public void run() { 
               proceed(); 
            } 
          }); 
      } 
   } 
   declare error: @annotation(EventThread) 
     && execution(!void *.*(..)) : "Must return void"; 
}

Listing 9.9    Aspekt für Zuordnung von Threads zur Ausführung einer Methode

Methoden invokeLater und invokeAndWait

Grundsätzlich haben wir zwei Möglichkeiten, wie wir den Aufruf in den AWT-Ereignisthread verschieben können:

  • durch die Methode invokeLater
  • durch die Methode invokeAndWait

Beide Methoden sorgen dafür, dass der Aufruf im AWT-Ereignisthread abgearbeitet wird, nachdem alle bereits vorliegenden Ereignisse abgearbeitet worden sind. Der Unterschied zwischen den beiden Methoden besteht darin, dass die Methode invokeLater sofort zurückkehrt, während invokeAndWait so lange wartet, bis der Aufruf im AWT-Ereignisthread bearbeitet wurde. In unserem Beispiel haben wir uns für den Einsatz von invokeLater entschieden, da invokeAndWait mehr Aufwand erfordert, um Deadlocks zu vermeiden.

Nur Methoden ohne Rückgabewert

Da wir aber möglicherweise noch vor dem eigentlichen tatsächlichen Aufruf der urspünglichen Methode zurückkehren, können wir deren Ergebnis nicht liefern. Aus diesem Grunde erlauben wir den Einsatz der Annotation @EventThread nur für Methoden, die void zurückgeben. Diese Einschränkung wird in der Sektion, die mit declare error beginnt, vorgenommen.

Um zu zeigen, wie sich die neu eingeführte Annotation EventThread im Einsatz verhält, konstruieren wir in Listing 9.10 in der Zeile ein Swing-Fenster, das eine Textzeile mit einer Zahl und einen Button enthält.

Durch Klick auf den Button können wir den Wert der dargestellten Zahl um 10 erhöhen. Diesen Button konstruieren wir in der Zeile , er bekommt dabei die in Zeile definierte Aktion zugeordnet.

public class TestFrame extends JFrame { 
   private JLabel outputLabel; 
   private int counter; 
 
   public TestFrame() {         
      JPanel pane = new JPanel(new BorderLayout()); 
      setContentPane(pane); 
      outputLabel = new JLabel(); 
      pane.add(outputLabel, BorderLayout.CENTER); 
      outputLabel.setText("-"); 
      Action incAction = new AbstractAction("Add 10") {   
         public void actionPerformed(ActionEvent e) { 
            add(10); 
         } 
      }; 
      pane.add(new JButton(incAction),          
                      BorderLayout.SOUTH); 
      validate(); 
      pack(); 
      Thread backgroundAdder = new Thread(new Runnable(){  
         public void run() { 
            while (true) { 
               try { 
                  Thread.sleep(1000); 
               } catch (InterruptedException ignored) { 
               } 
               addOne(); 
            } 
         } 
      }); 
      backgroundAdder.start(); 
   } 
 
   @EventThread 
   private void addOne() {   
      System.out.println("addOne in " + 
              Thread.currentThread().getName()); 
      counter++; 
      outputLabel.setText(Integer.toString(counter)); 
   } 
 
   @EventThread 
   private void add(int n) {  
      System.out.println("add in " + 
               Thread.currentThread().getName()); 
      counter += n; 
      outputLabel.setText(Integer.toString(counter)); 
   } 
 
   public static void main(String[] args) { 
      TestFrame f = new TestFrame(); 
      f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); 
      f.setVisible(true); 
   } 
}

Listing 9.10    Anzeige eines Zählers

Ab Zeile von Listing 9.10 konstruieren und starten wir einen neuen Thread backgroundAdder, der unsere dargestellte Zahl über die Methode addOne jede Sekunde um 1 erhöht. Die Methode addOne selbst wird in Zeile implementiert. Schließlich sehen wir Zeile die Umsetzung der Methode add, die bei Klick auf unseren Button aufgerufen wird.

Synchronisationsproblem

Wenn wir nicht dafür sorgen, dass unsere beiden Methoden add und addOne grundsätzlich im AWT-Ereignisthread ausgeführt werden, haben wir ein Synchronisationsproblem. Käme der Klick auf den Button genau in dem Augenblick, in dem der nebenläufige Thread den Wert der Zahl um 1 erhöhen soll, kann es theoretisch passieren, dass die Textzeile einen falschen Wert anzeigt. Für unser aktuelles Beispiel scheint das nicht wirklich tragisch zu sein, in anderen Kontexten würde das aber ein größeres Problem darstellen. Wenn zum Beispiel über eine der Methoden Einfügungen in eine Listbox vorgenommen werden und in der anderen Methode auf diese Einträge während des Einfügens zugegriffen wird, kann der lesende Zugriff auf einen undefinierten Zwischenzustand treffen.

Um das Verhalten der Methoden verfolgen zu können, lassen wir zu Testzwecken die Methoden add und addOne den Thread, in dem sie ausgeführt werden, ausgeben. Wir spielen nun ein Szenario durch, bei dem wir zweimal auf unseren Button klicken und währenddessen unser Thread ebenfalls unseren Counter hochzählt.

Ausgabe ohne Annotation

Wenn wir die Annotation EventThread für die Methoden add und addOne nicht verwenden, erhalten wir zum Beispiel die unten stehende Ausgabe:

addOne in Thread-2 
addOne in Thread-2 
add in AWT-EventQueue-0 
addOne in Thread-2 
add in AWT-EventQueue-0 
addOne in Thread-2 
addOne in Thread-2

Die Methoden werden also immer in dem Thread gestartet, von dem aus sie aufgerufen werden. Da ein Klick auf einen Button bereits im AWT-Ereignisthread verarbeitet wird, erfolgt auch die Abarbeitung unserer Methode add in diesem Thread. Die Methode addOne wird in dem von uns selbst gestarteten Thread aufgerufen, also erfolgt auch ihre Abarbeitung dort.

Ausgabe mit Annotation

Wenn wir aber wie in unserem Listing die Annotation EventThread für beide Methoden verwenden, werden alle Aufrufe unserer Methoden im AWT-Ereignisthread ablaufen. Die Ausgabe unseres Programms sieht dann zum Beispiel aus wie folgt.

addOne in AWT-EventQueue-0 
addOne in AWT-EventQueue-0 
add in AWT-EventQueue-0 
addOne in AWT-EventQueue-0 
add in AWT-EventQueue-0 
addOne in AWT-EventQueue-0 
addOne in AWT-EventQueue-0

Annotations unterstützen Trennung der Anliegen

Die Kombination der zusätzlichen Metainformationen durch die Annotations mit der Aspektorientierung bietet uns also neue Möglichkeiten für die Erweiterung der verwendeten Programmiersprachen, mit deren Hilfe wir unsere Quelltexte besser strukturieren können. Wir haben hier also ein weiteres Mittel, um die Trennung von Anliegen in unseren Quelltexten vorzunehmen. In unserem Beispiel müssen wir zwar immer noch angeben, welche unserer Methoden im separaten Ereignisthread laufen sollen. Dieses Anliegen wird aber nun zu einer Eigenschaft der Methode und ist nicht mehr dem Aufruf von Methoden zugeordnet. Außerdem haben wir die Umsetzung der Verlagerung in einen eigenen Thread zentral als einen Aspekt umgesetzt.



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