Inhaltsverzeichnis[Anzeigen]
Inhaltsverzeichnis[Anzeigen]

 

Roter Faden

Bestseller

Als pragmatischer Ansatz bietet es sich an, sich die am häufigst verwendeteten Designpattern (gelb) anzuschauen. Ich werde die Liste noch um das eine oder andere Pattern erweitern. DesignPatternRelation.png
Da der einfachste Zugang zu Design Patterns in ihrer Anwendung liegt, werde ich ein einfaches Dateisystem aus der Sicht eines Anwenders entwickeln.
Das heißt insbesondere, daß ich ein fleischlosses Filesystem, ein Interface für ein Dateisystem entwickeln will.
Die Idee, motivierend Design Pattern mittels des Entwurfs eines Dateisystems einzuführen, stammt von John Vlissides aus seinem C++ Buch Pattern Hatching, einem Mitglied der GOF. Als proof of concepts habe ich alle Beispiele in Java geschrieben. Unter dem Punkt Nutzenversuche ich die Vorteile des Patterns in unserer konkreten Situation aufzuzeigen.

ALERT! Ziel meines Dateisystems soll nicht die Anwendung Filesystem, sondern der Zugang zu den Design Pattern sein.

Entwurf eines objektorientierten Dateisystems

  • Die Sicht auf das Dateisystem ist relativ, da sie immer von Objekten - Verzeichnissen und Dateien - aus möglich ist.
  • Frägt man das Dateisystem mittels des Wurzelknotens
    myRoot
    ab, so erhält man eine absolute Sicht auf das Dateisystem.

Kompositum Pattern

Meine ersten Forderungen an das Dateisystem sind, das
  • die zwei Elementtypen Dateien und Verzeichnisse unterstützt
  • eine Baumstruktur erzeugt wird
  • Zugriffe auf die Elementtypen - ob Einzeltyp Datei oder Kompositum Verzeichnis - unabhängig vom Typ vollzogen werden
ALERT! Gerade der letzte Punkt bedeutet, daß folgender Code vermieden werden sollte.
if ( typeElement.equals("Datei") ){

// Dateispezifisch
}
if ( typeElement.equals("Verzeichnis ) {

// Verzeichnisspezifisch
}
MOVED TO...Damit die Aufrufe transparent vollzogen werden können, müssen die beiden Typen das gleiche Interface unterstützen:
  • daher ergibt sich folgendes Minimalgerüst des Kompositumpattern
interface Node{}

public class File implements Node{}

public class Dir implements Node{
private List children;

}
  • eine Variation des Kompositumpatterns besteht darin, eine abstrakte Basisklasse als Interface zu benützen
abstract class Node{


public void add( Node ){};
public void doThis();

}
;

public class File extends Node{}

public class Dir extends Node{

public add ( Node ) { //add };

public void doThis(){

Iterator childrenIter = children.iterator();
while( filesIter.hasNext() ) childrenIter.doThis();

}

private List children;
}
  • das entsprechende UML Klassendiagramm hat folgendes Aussehen:
    composite.jpg

Und nun zur Praxis:

  1. Das Interface von Knoten MyNode.java enthält Methoden
    • zur Hinzufügen von Knoten addNode( Node node )
    • zur Identfizierung des Knotens
       getPath()
      getName()
    • zur Bekanntgabe seiner Identität show()
  2. die fleischlose Implementierung von addNode ist für MyFile.java genau richtig
  3. deutlich mehr Aufwand muß in MyDir.java investiert, werden:
    • die Kindern müssen verwaltet werden
    • show() muß an die Kinder weiterpropagiert werden
  4. in GenerateFileSystem.java erzeuge ich ein virtuelles Dateisystem mit dem Java Interface
    • Variablen, die mit system beginnen, sind java Typen
    • die Verbindung der Knoten erfolgt für
      • Dateien
        • node.addNode(new MyFile( systemFile.getName(), node.getPath() ) )
      • Verzeichnisse
        • MyDir newDir= newMyDir(systemFile.getName(),
        • node.getPath()+MyFileSystem.SUBDIR+
        • systemFile.getName() );
        • node.addNode( newDir );
      • für die Verzeichnisstrukturen mittels Rekursion über getFileListing( newDir )
  5. ruft man nun java MyNode dir , erhält man modulo Exception in thread "main" java.lang.OutOfMemoryError das ganze Verzeichnis aufgelistet
  6. wähle ich für dir /home/grimm/TestFramework statt /home/grimm, dann ergibt sich folgende Ausgabe auf der Konsole:
    -------------------------Template Method
    DIR: PATH : TestFramework NAME :TestFramework
    DIR: PATH : TestFramework/java NAME :java
    DIR: PATH : TestFramework/java NAME :java
    DIR: PATH : TestFramework/java/fitnesse NAME :fitnesse
    FILE: PATH : TestFramework/java/fitnesse NAME :path.elk
    FILE: PATH : TestFramework/java/fitnesse NAME :license.txt
    DIR: PATH : TestFramework/java/fitnesse/dotnet NAME :dotnet
    FILE: PATH : TestFramework/java/fitnesse/dotnet NAME :TestRunner.exe
    FILE: PATH : TestFramework/java/fitnesse/dotnet NAME :FitServer.exe
    ........
    FILE: PATH : TestFramework/usr/lib/python2.2/site-packages/qm NAME :extension.pyc
    FILE: PATH : TestFramework/usr/lib/python2.2/site-packages/qm NAME :__init__.py
    FILE: PATH : TestFramework/usr/lib/python2.2/site-packages/qm NAME :common.pyc
    FILE: PATH : TestFramework/usr/lib/python2.2/site-packages/qm NAME :temporary_directory.pyc
    FILE: PATH : TestFramework/usr/lib/python2.2/site-packages/qm NAME :platform.py
    FILE: PATH : TestFramework/usr/lib/python2.2/site-packages/qm NAME :db.pyc
    DIR: PATH : TestFramework/usr/lib/python2.2/site-packages NAME :site-packages
    DIR: PATH : TestFramework/usr/lib NAME :lib
    FILE: PATH : TestFramework NAME :BeautifulSoup.py
    FILE: PATH : TestFramework NAME :Projekt.txt
Nutzen:
  • durch das Kompositium Pattern können wir die show Methode transparent prozessieren; d.h.: unabhängig vom konkreten Datentyp
  • während die Aufrufe auf der Dateiebene direkt evaluiert werden, werden die Aufrufe auf der Verzeichnisebene weiterpropagiert
Betrachten wir die Methode show() genauer, so stellt sie rein zufällig einen idealen Übergang zum nächsten Pattern dar.
class MyNode{
public void show(){

System.out.println( getTyp() + " PATH : " + getPath() + " NAME :" + getName());

}
}
  • show() zeichnet sich durch folgende Struktur aus:
    • show() legt das Gerüst für die drei get Methoden bereit
    • durch virtuellen Dispatch werden die drei Methoden aufgelöst
    • getName wird auf MyNode selbst, getTyp und getPath() werden auf den MyDir oder MyFile abgebildet
    • MOVED TO... die Schablone show() gibt die Struktur vor, in der die drei get Methoden die spezifische Funktionalität bereitstellen

Template Methode

  • Die Schablonenmethode besitzt folgende Struktur:

TemplateMethod.jpg

  • TIP In der Sprache C++ ist dies Pattern auch als Non Virtual Interface (NVI) bekannt, da hier eine öffentliche, nicht virtuelle Methode die Struktur des Alogrithmus vorgibt und private bzw. protected virtuelle Methoden die Funktionalität implementieren. Durch die Trennung von Interface (öffentliche, nicht virtuelle Methode) und Implementierung (protected oder private virtuelle Methode) gibt gerade Non Virtual Interface (NVI) ein stabiles Interface vor, das nur Variation auf den virtuellen Methoden erlaubt. Im Gegensatz hierzu stellt java die Templatemethode sowohl als Interface wie auch die Implementierung bereit, da java nur dynamische Bindung kennt.

Nutzen:

  • legt die Struktur eines Algorithmus in der Basisklasse fest
  • unterstützt die Wiederverwendung von Methoden durch die Identifizierung der variablen und konstanten Komponenten des Algorithmus
  • fördert die leichte Erweiterbarkeit von bestehenden Klassenhierachien, da lediglich die virtuelle Methode überschrieben werden muß um den Algorithmus richtig zu prozessieren
Einschränkungen:
  • Falls die Klasse MyFile verschieden Arten anbieten will sich zu zeigen, muß für jede Variante eine neue Klasse geschrieben werden.
    • Dies kann zu einem kombinatorischen Problem führen, wie std::string verdeutlicht.
      • std::string wird durch seinen Zeichentyp, seinen Umgang mit den Zeichen und seiner Speicherallokierung bestimmt. Falls ich nur annehme, daß für jede Komponente von std::string 4 verschieden Möglichkeiten existieren, müsste ich 4*4*4= 64 verschieden Klassen implentieren, um diese Komplexität abzubilden.
    • Die Lösung besteht darin, die drei Komponenten in je ein eigenes Objekt auszulagern, so daß wir im wesentlichen 4+4+4= 12 Klassen implementieren müssen.
  • Ein vielleicht noch größerer Einschränkung besteht bei der Schablonenmethode darin, daß die Struktur zur Compilezeit schon fest liegt. Die Objekte könnnen daher nicht zu Laufzeit ihren Algorithmus ändern.
MOVED TO...Wegen dieser zwei Argumenten will ich das Strategiepattern vorstellen, das eine statische Verhaltenswahl durch eine dynamische ersetzt.

Strategy Pattern

Aus der obigen Erörterung ergibt sich der Einsatz des Strategiepatterns bei folgender Konstallation:
  • das Verhalten eines Objekts sollte sich zu Laufzeit verändern lassen
  • mehrere verwandte Klassen unterscheiden sich nur in ihrem Verhalten
MOVED TO...Ich will beispielhaft annehmen, daß es Dateisystembenutzer gibt, die folgende Sichten auf das Dateisystem dynamisch konfigurierbar benötigen:
  1. die bekannte Sicht aus der Templatemethode
  2. alle Dateien mit Pfadangabe
  3. alle Verzeichnisse mit Pfadangabe
  4. die Namen und Pfadangaben aller Einträge
Diese Flexibilität erreichen wir durch folgenden Struktur:
  • der Client, das main-Programm nützt die Strategien
    • mittels setShowStrategy wird die Stratgie gesetz; default ist ShowTypNamePathStragie
    • der Aufruf showWithStrategie erzeugt die entsprechende Sicht
    • beide Aufrufe werden durch das alle Unterknoten propagiert
myRoot.setShowStrategy( new ShowFileStrategy() );
  • jeder Knoten erhält eine Stratgieobjekt, an den es die show Aufgabe delegiert
  • ShowStrategie legt das Interface aller zu unterstützenden Sichten fest
public interface ShowStrategy{

public void show( MyNode node );
}
  • das Interface wird in den konkreten Strategieklassen implementiert
    • der Knoten übergibt sich an das Strategieobjekt, damit dies Objekt Zugriff auf die Daten des Knoten erhält
Folgende Ausgabe wird nun durch die Strategien erzeugt:
-------------------------Template Method
DIR PATH : NAME :TestFramework
DIR PATH : java NAME :java
DIR PATH : java/fitnesse NAME :fitnesse
FILE PATH : java/fitnesse NAME :path.elk
....
-------------------------StrategyPattern: Type + Path + Name
DIR PATH : NAME :TestFramework
DIR PATH : java NAME :java
DIR PATH : java/fitnesse NAME :fitnesse
FILE PATH : java/fitnesse NAME :path.elk
....
-------------------------Strategy Pattern: File
FILE : java/fitnesse/path.elk
....
-------------------------Strategy Pattern: Dir
DIR :
DIR : java
DIR : java/fitnesse
DIR : java/fitnesse/dotnet
DIR : java/fitnesse/FitNesseRoot
....
-------------------------Strategy Pattern: Name + Path
PATH : NAME :TestFramework
PATH : java NAME :java
PATH : java/fitnesse NAME :fitnesse
PATH : java/fitnesse NAME :path.elk
Nutzen:
  • Durch die Kapslung des variierenden Aspekts (hier show) in ein Objekt und die Delegation dieses Aspekt an das Stratgieobjekt, entsteht ein dynamisch konfigurierbares Verhalten.


Klassischerweise wird das Strategieobjekt anhand eines dynamisch austauschbaren Alogorithmus ( hier sort ) erläutert.
Strategy Patterns

Iterator

  • Betrachten wird die sich rekursive aufrufenden Methoden wie show(), setShowStrategy() oder showWithStrategy() in MyDir.java, dann fallen zwei Punkte sehr negativ auf:
    1. Redundanz der Tranversierungslogik
    2. Vermischung von Traversierung und Prozessierung der Knoten
  • Beide Punkte lassen sich durch ( interne oder externe ) Iteratoren elegant lösen.
  • Der wesentlicher Unterschied dieser Iteratortypen besteht darin, daß externe Iteratoren von außen, also vom Client gesteuert werden, während interne Iteratoren vom Client lediglich angestossen werden.

Interne Iterator

  • Die drei oben genannten show... Methoden stellen interne Iteratoren dar, denn einmal vom Client aufgrufen, arbeiten sie automatisch die Knotenhierachie ab.
  • Ersetzen wir in show() die Aufrufe super.show() bzw. tmpNode.show(), durch die ein Objekt, an das der variierende Aspekt delegiert wird, wenden wir wiederrum das Strategiepattern an.
class MyDir{
public void show(){


Iterator childIter = getChildrenIterator();

super.show();

while( childIter.hasNext() ){
MyNode tmpNode= (MyNode) childIter.next();

tmpNode.show();
}
}
}
  • Die Parametrisierung der Traversierung der Knotenhierachie mittels eines Strategieobjekts ( oder Functor in C++ ) schien mir aber schwieriger ( unvertrauter ) als die Einführung eines externen Iterators.

Externe Iterator:

  • sind wohlbekannt
    1. C++
       for (it= list.begin(); it != list.end(); ++it )...     
    2. Python
      for i in [1,2,3,4]:
      ...
    3. Java
       while ( nodeIt.hasNext()){
      nodeIt.next(); ...
  • Durch Erweiterung der Knotenhierachie um die öffentliche Methode getDescendantsIterator(), die die Implementierungsmethode appendDescendants in MyDir.java anstösst, ist es nun möglich, einen Iterator über alle Unterknoten eines beliebigen Knotens zu erhalten.
  • Um die Methode doSomething() auf alle Unterknoten von newNode einschließlich newNode auzuführen, muß folgender Code vom Client prozessiert werden.

class MyDir{
Iterator nodeIt= myRoot.getDescendantsIterator() ;


while ( nodeIt.hasNext()){
MyNode tmpNode= (MyNode )nodeIt.next();

tmpNode.doSomething()
}
}
  • Zwar unterstützt das java Interface Iterator die remove() Methode, stellt ab keine Methode bereit, um den Iterator wieder zu initialisieren.
  • Iteratoren werden durch die zu iterierenden Container erzeugt.
  • Insofern beschreibt folgendes Klassendiagramm eine Varianz des externen Iterators.
  • Die Einführung einer Schnittstelle sowohl über dem Iterator als auch dem zu iterierenen Container entkoppelt den Client vollkommen von den Internas des Containers wie auch der Traversierungsstrategie. So können Iterator implementiert werden, die rückwärts iterieren, filternd iterieren .... .
    Iterator Pattern

 

Nutzen:

  • Der exteren Iterator ermöglicht es, die Traversierung des Dateisystems von der Prozessierung der einzelnen Knoten zu trennen, so daß jede Methode eine genau defininiert Funktionalität besitzt und Coderedundanz vermieden werden kann.


Die Knotenmethode getDescendantsIterator, die einen Iterator über das Dateisystem zur Verfügung stellt, nennt man in der Patternterminologie Fabrikmethode. Bevor ich aber auf dieses Pattern genauer eingehe, benötigt ich noch eine kleine Erweiterung und eine weitere Abstraktion.

Exkurs:
  • Um ein differenziererende Sicht auf das Dateisystem zu erhalte, sollen die Knoten nun über ihren eindeutige Id aus Pfad und Namen ansprechbar sein.
  • Als Konsequenz aus dem objektorientierten Dateisystem kann es nur eine relative Sicht auf die Knoten geben.
  • Frägt man das Dateisystem mittels des Wurzelknotens
    myRoot
    ab, so erhält man eine absolute Sicht auf das Dateisystem.
  • Dazu werde ich das MyNode Interface um die Methode
     getNode( String str ) 
    erweitern um mittels eines Strings den Unterknoten explizit anzusprechen.
  • Damit wird es möglich sein, selektiv Knoten anzusprechen, um sie zu kopieren, zu löschen oder auch zu bewegen.


Fassade Pattern

Um copy in der Form copy(Ursprung,Ziel) zu ermöglichen, fehlt mir noch eine Abstraktion über dem Knotenbaum. Denn mit der bisherigen Abstraktion zerfällt jede dieser Operation in mehrer Operationen auf dem Knotenbaum.

MOVED TO... Was liegt also näher, als eine Abstraktion MyFileSystem.java einzuführen.

Diese Abstraktion legt sich wie eine Fassade über die bisherige Knotensystem, bietet elementare Operationen auf einem höheren Abstraktionsgrad an und erleichtert somit die Verwendung der bisherigen Funktionalität.
Die bisherige Funktionalität, das Subsystem, das MyFileSystem.java instrumentalisiert, werde ich auch nun konsequenterweise in einem eigene Paket fileSystem verpacken.

TIP Durch die Einführung eines Interfaces FileSystem, gegen das ich MyFileSystem.java programmiere, könnte man die Fassade sogar zur Laufzeit austauschen und das Abstract Factory Pattern einführen.
ALERT! Wenn es auch kein guter Stil ist, kann der Client immer noch die ganze Funktionalität des Subsystems nützen, indem er die Fassade umgeht und direkt mit dem Subsystem, dem Knotenbaum, kommuniziert.

Nutzen:
  1. die umständliche und fehleranfällige Knotentraversierung wird gekapselt
  2. um über Teilbäume zu iterieren müssen nicht explizit die Knoten instanziert werden ( vgl. myRoot.getNode(...) )
  3. der Client ist nicht mehr von dem ganzen Knotensytem, sondern nur noch von der Abstraktion MyFileSystem abhängig

 

Facade Pattern


Da die Sicht auf die Knotenhierachie relativ ist, soll dies auch für MyFileSystem gelten. Daher wird MyFileSystem mit der relativen Sicht eines Wurzelverzeichnisses initialisiert.

Singleton

ALERT! Ein wichtiger Aspekt des Singleton Patterns ist es, den Zugriff auf das Objekt zu kontrolliern und somit möglichen Mißbrauch vorzubeugen.
Gerade aus diesem Grund, die Nutzung der Knotenhierachie zu vereinfachen, führte ich das Fassade Pattern ein.
Daher will ich Nutzung nur einer FileSystem-Abstraktion zulassen, denn folgender Code kann leicht verwirren und bietet keinen Mehrwert.
MyFileSystem myFileSystem1= new MyFileSystem( subDir1 );  
MyFileSystem myFileSystem2= new MyFileSystem( subDir2 );

myFileSystem2.remove( file2 ); // Exception, da file2 kein Element von subDir2
Die Struktur des Singleton Patterns ist ( in java ) schnell erkärt:
  1. definiere den privaten Konstruktor
private Singleton() {}
  1. erkläre eine private statische Variable
    instance
    des Klassentyps
  2. definiere eine statische, öffentliche Methode
    getInstance()
    , die beim Aufruf die initialisierte Klasseninstanz instance zurückgibt
class MyFileSystem{

public static Singleton getInstance(){
if ( instance == null ) { instance= new Singleton(); };

return instance;
}
}
Nutzen:
  1. Durch einen Aufruf der Form Singleton.getInstance() greift der Client mittels der Klassenmethode (statischen Methode) auf die einzige Objektinstanz instance zu.
  2. Der Konstruktoraufruf wird in der Methode getInstance() beim ersten Aufruf vollzogen.
ALERT!Durch die einfache Struktur und das implizite Instanziierung des Objekts wird das Singleton wohl zu oft verwendent, um mittels OO Mitteln globale Funktionen und Variablen duch die Hintertür wieder zuzulassen.

Singleton Pattern

Fabrik Methode ( Virtuelle Konstruktor )

Neben der remove Operation soll mein Dateisystem auch eine move Operation unterstützen. Die Funktionalität werde ich in der Fassade MyFileSystem über dem Knotenbaum kapseln.
Der Client ruft folgenden Code auf, wobei moveFile und moveDir Knoten sind:
myFileSystem.move( moveFile, "absolutePathToNewFile");

myFileSystem.move( moveDir, "absolutePathtoNewDir");
Diese Anweisungen stossen dann jeweils die Methode move an:

class MyFileSystem{
public void move( MyNode sourceNode, String destination){

String[] pathAndName= getPathAndName( destination);

String path= pathAndName[0];
String name= pathAndName[1];

MyDir newParent= (MyDir) myRoot.getNode( path);
newParent.addNode( sourceNode.clone(newParent , name )); // <--- destination geht hier ein

sourceNode.getParentNode().removeNode( sourceNode.getName() );
}

}
Da es von virtuellen (dynamischen) Typ von sourceNode abhängt, ob eine Datei oder ein Verzeichnis nach destination bewegt wird, erwartet man im ersten Ansatz ein Code der Form:
if ( sourceNode.getTyp == FileSystem.FILE ) newParent.addNode( new MyFile( newParent, newParent.getPath(), name ));

else newParent.addNode( MyDir( newParent, newParent.getPath(),name ));
Darüber hinaus muß im Fall eines Verzeichnisses der ganze Teilbaum richtig initialisiert und verlinkt werden.

Genaus diese Arbeit wird durch den Ausdruck newParent.addNode( sourceNode.clone(newParent , name ))geleistet:
  1. sourceNode.clone(newParent, name) erzeugt ein neues Objekt MOVED TO... Fabrikmethode
  2. abhängig vom virtuellen (dynamischen) Typ von sourceNode, wird die clone Funktion von MyFile oder MyDir und somit der entsprechende Konstruktor aufgerufen MOVED TO... virtuelle Konstruktor
Folgendes Bild verdeutlicht die Struktur der Fabrikmethode. Bei unserem konkreten Anwendung fielen die Rollen des Produkts und des Erzeugers zusammen.
Fabrikmethode

Dies Pattern besitzt in zwei Kontexten grosse Vorteile:

  1. an vielen Stellen im Code wird unter mehreren Typen ausgewählt
  2. falls die Funktionalität ( vgl. move ) durch ein Framework angeboten wird, existiert keine Möglichkeit, ein neuen Typ zu unterstüzten, denn die notwendige if Anweisung kann im Framework nicht eingefügt werden MOVED TO... hingegen reicht es bei der Fabrikmethode aus, einen neuen Typ abzuleiten, der das clone Interface unterstüzt
Nutzen:
  1. eine fest codierte ( statische ) Entscheidung wird durch eine dynamische aufgelöst

Was ist ein Dateisystem ohne Links? Daher soll mein Dateisystem als weiteren Knotentyp Links anbieten.
Zwei Typen von Links werde ich unterstützen, symbolic und smart links, die sich durch folgende Eigenschaften auszeichnen.
  1. alle referenzieren einen Knoten, eine Ressource
  2. die Löschung des Links führt nicht zur Löschung der Ressource
  3. symbolische Links referenzieren den Knoten über seinen absoluten Pfad
    • sie werden ungültig, falls der Knoten an anderer Stelle oder mit anderem Namen im Dateisystem aufgehängt wird
    • ihnen können andere Knoten mit dem gleichen absoluten Pfad untergeschoben werden
  4. smart Links referenzieren das Knotenobjekt
    • verschieben des Ressourceknotens direkt über die Datei oder indirekt über ein Verzeichnis invalidieren den smart link nicht
MOVED TO...Charakteristisch für Links ist somit:
  • sie sind Stellvertreter ihrer Ressource
  • insbesondere smart Links müssen ihre Ressource beobachten
Da die Linktypen doch einiges gemeinsam haben, will ich das Dateisystem um eine abstrakte Klasse Link erweitern, die die konkreten Untertypen SymbolikLink und SmartLink bieten soll.

Proxy

Proxys sind Stellvertreter, die den Zugriff auf eine andere Ressource kapseln.
Es gibt viele Varianten von Proxys, so den
  • remote Proxy, als lokalen Stellvertreter für eine entfernte Ressource
  • virtuellen Proxy, als Objekt, das das teuere Objekt erst auf Verlangen erzeugt
  • Schutzproxy, als Zugriffskontrolle aus die Ressource
  • Smart Referenz, als intelligenten Zeiger.
Symbolische Links sind sowohl einerseits remote Proxies, da sie den Zugriff auf eine entfernte Ressource unterstützen, als auch andererseits virtuelle Proxies, da sie das neue Anlegen von Dateien oder Verzeichnissen überflüssig machen.

Mittels
public class MySymbolicLink extends MyLink{

protected String getText(){

MyFileSystem.getInstance().getRoot().getNode( ressource ).getText();
}
ruft der symbolische Link in
getText()
die
getText()
Methode seiner Ressource auf.

Charakteristisch für das Proxy Pattern ist es, daß die Ressource und der Stellvertreter das gleiche Interface anbieten, gegen das der Klient seine Anfrage stellt. Der Proxy delegiert die Anfrage an die Ressource weiter.
Proxy Pattern

Dieser erste Ansatz eines symbolischen Links lässt doch viele Wünsche offen, da der Stellvertreter nichts über Veränderungen der Ressource, wie umbenennen oder löschen, erfährt:

class Application{
main(String[] args) {

myFileSystem.createLink( ressourceNode, linkNode ,MyFileSystem.SYMBOLIC_LINK);
myFileSystem.remove(ressourceNode);

try{
System.out.println("myFileSystem.getText(linkNode) : " + myFileSystem.getText(linkNode));

}
catch ( Exception e ){
System.err.println(e.getMessage());

}
}
}
Dieser Code führt insofern zu einer Ausgabe auf System.err: Couldn't find Projekt.txt
Nutzen:
  • Die Ressource Knoten wird nur einmal angelegt, aber öfters referenziert.
Um einen intelligentere Referenz - eine smart Referenz - zu unterstüzten, registrierte ich die Referenz als Beobachter der Ressource.

Observer Pattern

Knoten sollen ihre Veränderungen im Dateisystem wie umbenennen oder verschieben an ihre smart Links mitteilen. Diese smart Links agieren als Beobachter ihres Subjekts Ressource.
Folgende zwei Beispiele verdeutlichen die Mächtigkeit von smart Links:
  1. umbenennen der Datei Projekt.txt, auf die der smart Links verweist:
    create smart link from C++/SpielWiese/Gui/Elkin/ABSOLUTELY/web/Projekt.txt.smartLink to Projekt.txt
    move file : from Projekt.txt to C++/SpielWiese/Gui/Elkin/ABSOLUTELY/web/ProjektNeu.txt
    myFileSystem.getText(C++/SpielWiese/Gui/Elkin/ABSOLUTELY/web/Projekt.txt.smartLink) : NAME OF RESSOURCE: ProjektNeu.txt
  2. umbenennen des Verzeichnisses, in dem die Ressource ProjektNeu.txt liegt
    create smart link from newSmartLink.txt to C++/SpielWiese/Gui/Elkin/ABSOLUTELY/web/ProjektNeu.txt
    move dir : from C++/SpielWiese/Gui/Elkin/ABSOLUTELY/web to C++/SpielWiese/Gui/Elkin/ABSOLUTELY/webNEW
    myFileSystem.getText( newSmartLink): NAME OF RESSOURCE: ProjektNeu.txt
Wie funktioniert nun das Beobachter Pattern?

statisch

Es gibt zwei Rollen im Beobachter Pattern, die des Beobachter und die des Subjekts, das sich beobachten lässt.
  1. Das Subjekt der Beobachtung, der Knoten, implementiert des Observable Interface, wobei die Methode removeAllObserver nicht zwingend notwendig ist
public interface Observable {

public void addObserver( MyNode observerNode );

public boolean removeObserver( MyNode node);
public void removeAllObserver();

public void notifyObserver();

}
  1. Der Smart Link implementiert das Observer Interface
public interface Observer {

public void update( MyNode node);

}

dynamisch

Initialisierung
  • das Dateisystem legt einen smart newSmartLink auf newRessourceFile an
myFileSystem.createLink( newRessourceFile, newSmartLink, MyFileSystem.SMART_LINK );
  • im Konstruktor des smart Links meldet sich dieser bei der Ressource mittels addObserver an
public MySmartLink( MyDir parent, String name , String ressource){


super( parent, name);
ressourceNode= MyFileSystem.getInstance().getRoot().getNode( ressource );

ressourceNode.addObserver( this );
}
Laufzeit
  • die Ressource ändert direkt oder indirekt mittels der Methode move ihren Namen, so daß für alle Unterknoten die Methode notfiyObserver aufgerufen werden muß
class MyFileSystem{
...
public void move( String source, String destination){


MyNode sourceNode= myRoot.getNode(source);
copyImplementation( sourceNode, destination);
// notify all descendants

MyNode destinationNode= myRoot.getNode( destination);
destinationNode.notifyObserver();
Iterator nodeIt= destinationNode.getDescendantsIterator() ;

while ( nodeIt.hasNext()){
MyNode tmpNode= (MyNode )nodeIt.next();

tmpNode.notifyObserver();
}
sourceNode.getParentNode().removeNode( sourceNode.getName() );

}
}
  • die Ressource übergibt sich in notifyObserver selber durch update an den Beobachter
public void notifyObserver(){

Iterator obsIter= allObserver.iterator();
while ( obsIter.hasNext()){

MyLink link= (MyLink) obsIter.next();
link.update( this );

}
  • der Beobachter aktualisiert nun seine Ressource
public void update( MyNode node ){


ressourceNode= node;

}
Die klassische Anwendung des Observer Pattern findet sich im Model-View-Control Pattern wieder, indem der Beobacher (View - die Datenvisualisierung -) von dem Model (den Daten ) auf dem aktuellen Stand gehalten wird.
Für die Aktualisierung des Beobachters bieten sich zwei Möglichkeiten an:
  1. push: das Subjekt übergibt die Daten beim benachrichtigen des Beobachter
  2. pull: das Subjekt meldet dem Beobachter nur die Veränderung und dieser holt sich dann die Daten vom Subjekt
Folgendens Klassendiagram beschreibt die zweite Variation des Observer Patterns.
Observer
Nutzen:
  • Der smart Link bleibt mit der Ressource Knoten synchronisiert.

Visitor Pattern

Ausgehend von den internen Iteratoren, bei denen ich die Traversierungs- mit der Operationslogik vermischt habe, will ich diese beiden Aspekte voneinander trennen. Um dies zu erreichen, bietet sich das Visitor Pattern an.
Die Operationen, die auf den Knoten angewandt werden sollen, werden als Besucher gekapselt und können somit die Knotenhierachie traversieren. Da jeder Besucher wissen muß, wie er sich bei einem konkreten Knoten - Verzeichnis, Datei, symbolische oder smart Links - zu verhalten hat, ist dies Pattern genau dann sinnvoll, wenn die Knotenhierachie stabil und lediglich verschieden Alogrithmen aus dieser Struktur angeboten werden sollen.

  • Um mir Schreibarbeit zu sparen, verwende ich als Visitor eine konkrete Klasse mit leeren Defaultimplementierung.
package fileSystem;

public class Visitor {

public void visit(MyDir dir){};

public void visit(MyFile file){};
public void visit(MySymbolicLink symLink){};

public void visit(MySmartLink smartLink){};
public void apply();

}

 

Modifikation

  • Mein Besucher soll Information über die Knotenstruktur zur Verfügung stellen und gegebenfalls auch Modifikationen auf der Knotenstruktur ausführen können.
  • Sowohl die Information über das Knotensystem als auch die Modifikation auf dem Knotensystem können erst nach der Traversierung der Knoten angewandt werden.
  • Aus diesem Grund erweitere ich die Basisklasse um die Methode
    public void apply() 
    .
  • Erst nach vollständiger Traversierung der Knotenhierachie wird die Methode ausgeführt.
public class MyFileSystem {
...

public void process( String dir, Visitor visitor){
myRoot.getNode( dir ).accept(visitor);

visitor.execute();
}
}
  • Jeder Knoten muß aber auch das Host Interface unterstützen, damit der virtuelle Dispatch auf den richtigen Typ stattfindet.
package fileSystem;

public interface Host {
void accept(Visitor visitor);

}

Beispiele

  1. lösche alle Dateien, die auf expr enden:
String expr= "~";

System.out.println("---------------------- remove all files ending with: " + expr);
myFileSystem.process( "", new CleanUpVisitor(expr) );

System.out.println("---------------------- remove all files ending with: " + expr);
myFileSystem.process( "", new CleanUpVisitor(expr) );
ergibt:
---------------------- remove all files ending with: ~
removeNode _initialize.cpp~ from dir C++/SpielWiese/CommandLine/elkin/sc/src/elkin/vfs/build/LittleEndian
removeNode Makefile~ from dir C++/HomeGuard-gcc/AllTestSuite
removeNode Elkin.pro~ from dir C++/SpielWiese/Gui/Elkin
...
removeNode Calculator.cpp~ from dir C++/SpielWiese/Gui/Calculator/lib
  1. prüfen auf dead links und gegebenenfalls löschen des Links:
System.out.println("---------------------- remove all dead links");   
myFileSystem.process( "", new CleanDeadLinksVisitor() );
ergibt:
---------------------- remove all dead links
removeNode Projekt.txt.symLink from dir C++/SpielWiese/Gui/Elkin/ABSOLUTELY/webNEW

  1. Auskunft über die konkreten Knotentypen
System.out.println("---------------------- count all node types: ");   

myFileSystem.process("", new CountNodeTypesVisitor() );
ergibt:
---------------------- count all node types: 
There are:
950 directories
5905 files
0 symbolic links
2 smart links

Struktur

statisch
Folgendes Klassendiagram verdeutlicht die Struktur des Visitor Patterns:
Visitor Pattern
  • es existieren zwei Ableitungshierachie
    1. die Gastgeber, die den Besucher aufnehmen wollen und daher die Methode ==accept()==implementieren müssen
    2. der Besucher, der eine spezifische Operation auf jedem Gastgeber ausführen wollen und insofern jeden Gastgeber auf eine besondere Art besuchen MOVED TO...die Besucher müssen die
      visit()
      Methode für jeden Gastgeber anbieten
  • in meinem konkreten Fall repräsentierten die Knotentypen die Gastgeber und die Operation auf den Knoten die Besucher
dynamisch
Damit der richtige Methode des richtigen Besucher aufgerufen wird, müssen zwei virtuelle Auflösungen in jeder Gastgebermethode accept() angestossen werden.
public void accept( Visitor visitor ){   
visitor.visit(this);
  1. der erste virtuelle Dispatch findet auf dem Visitor (visitor) Typ statt: CleanUpVisitor, CleanDeadLinksVisitor oder CountNodeTypesVisitor
  2. der zweite virtuelle Dispatch findet auf dem Knoten Typ (this) statt : MyFile, MyDir, MySymbolicLink oder MySmartLink=
Konkretes Beispiel
  1. die Applikationen nutzt myFileSystem, um alle Knoten unterhalb von root("") mit dem Visitor CountNodeTypesVisitor zu besuchen
VisitormyFileSystem.process("", new CountNodeTypesVisitor() );
  1. die Filesystemabstraktion übergibt einerseits den Visitor der Knotenhierachie und frägt andererseits die Ergebnisse der Prozessierung ab
myRoot.getNode( dir ).accept(visitor);

visitor.apply();
  1. in der acceptMethode findet nur der zweifache virtuelle Dispatch in zwei Variationen statt
    • MyFile, MySymbolikLink und MySmartLink verhalten sich wie oben beschrieben
    • MyDir muß den Dispatch zusätzlich an alle Kinder weiterpropagieren
public void accept( Visitor visitor){   
Iterator childIter= getChildrenIterator();

visitor.visit(this);
while( childIter.hasNext() ){

MyNode tmpNode= (MyNode) childIter.next();
tmpNode.accept ( visitor );

}
  1. unter der Annahme, daß der Visitor vom dynamischen Typ CountNodeTypesVisitor und this vom dynamischen Typ MyDir ist, wird folgende Funktion prozessiert:
public void visit(MyDir dir ){ dirCounter++;}
  1. die Methoden apply dient als Interfacevereinbarung, mit der man die Information des Visitors auswerten kann
  • Besonderheiten:
    1. Double-Dispatch-Mechanismus:
      • Für diese doppelte Virtualität hat sich der Name Double-Dispatch-Mechanismus etabliert.
    2. Internen Iterator:
      • Die accept() Methode von MyDir propagiert den Methodenaufruf an alle Kinder und somit auch an alle Nachkommen MOVED TO... sie stellt einen internen Iterator zur Verfügung
      • Durch das Visitor Pattern gelingt es nun aber im Gegensatz zu meinem obigen Code, die Traversierung von der Funktionalität zu trennen.
Nutzen:
  • leichte Erweiterung der Hierachie um weitere Operationen
  • Trennung der Traversierung von der Operation auf den Knote

Abonniere den Newsletter (+ pdf Päckchen)

Beiträge-Archiv

Sourcecode

Neuste Kommentare