Roter Faden
Bestseller
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.
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
if ( typeElement.equals("Datei") ){
// Dateispezifisch
}
if ( typeElement.equals("Verzeichnis ) {
// Verzeichnisspezifisch
}
- 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:
Und nun zur Praxis:
- 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()
- die fleischlose Implementierung von addNode ist für MyFile.java genau richtig
- deutlich mehr Aufwand muß in MyDir.java investiert, werden:
- die Kindern müssen verwaltet werden
- show() muß an die Kinder weiterpropagiert werden
- 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 )
- Dateien
- ruft man nun
java MyNode dir
, erhält man modulo Exception in thread "main" java.lang.OutOfMemoryError das ganze Verzeichnis aufgelistet - 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
- 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
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
- 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:
- 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
- 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.
- Dies kann zu einem kombinatorischen Problem führen, wie std::string verdeutlicht.
- 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.
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
- die bekannte Sicht aus der Templatemethode
- alle Dateien mit Pfadangabe
- alle Verzeichnisse mit Pfadangabe
- die Namen und Pfadangaben aller Einträge
- 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
-------------------------Template MethodNutzen:
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
- 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.
Iterator
- Betrachten wird die sich rekursive aufrufenden Methoden wie show(), setShowStrategy() oder showWithStrategy() in MyDir.java, dann fallen zwei Punkte sehr negativ auf:
- Redundanz der Tranversierungslogik
- 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
- C++
for (it= list.begin(); it != list.end(); ++it )...
- Python
for i in [1,2,3,4]:
... - Java
while ( nodeIt.hasNext()){
nodeIt.next(); ...
- C++
- Durch Erweiterung der Knotenhierachie um die öffentliche Methode
getDescendantsIterator()
, die die ImplementierungsmethodeappendDescendants
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 .... .
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 Formcopy(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. 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.
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.
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:
- die umständliche und fehleranfällige Knotentraversierung wird gekapselt
- um über Teilbäume zu iterieren müssen nicht explizit die Knoten instanziert werden ( vgl. myRoot.getNode(...) )
- der Client ist nicht mehr von dem ganzen Knotensytem, sondern nur noch von der Abstraktion MyFileSystem abhängig
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
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
- definiere den privaten Konstruktor
private Singleton() {}
- erkläre eine private statische Variable
instance
des Klassentyps - definiere eine statische, öffentliche Methode
getInstance()
, die beim Aufruf die initialisierte Klasseninstanzinstance
zurückgibt
class MyFileSystem{
public static Singleton getInstance(){
if ( instance == null ) { instance= new Singleton(); };
return instance;
}
}
- Durch einen Aufruf der Form
Singleton.getInstance()
greift der Client mittels der Klassenmethode (statischen Methode) auf die einzige Objektinstanzinstance
zu. - Der Konstruktoraufruf wird in der Methode
getInstance()
beim ersten Aufruf vollzogen.
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");
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() );
}
}
if ( sourceNode.getTyp == FileSystem.FILE ) newParent.addNode( new MyFile( newParent, newParent.getPath(), name ));
else newParent.addNode( MyDir( newParent, newParent.getPath(),name ));
Genaus diese Arbeit wird durch den Ausdruck
newParent.addNode( sourceNode.clone(newParent , name ))
geleistet:
sourceNode.clone(newParent, name)
erzeugt ein neues Objekt Fabrikmethode- abhängig vom virtuellen (dynamischen) Typ von sourceNode, wird die clone Funktion von
MyFile
oderMyDir
und somit der entsprechende Konstruktor aufgerufen virtuelle Konstruktor
Dies Pattern besitzt in zwei Kontexten grosse Vorteile:
- an vielen Stellen im Code wird unter mehreren Typen ausgewählt
- 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 hingegen reicht es bei der Fabrikmethode aus, einen neuen Typ abzuleiten, der das clone Interface unterstüzt
- 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.
- alle referenzieren einen Knoten, eine Ressource
- die Löschung des Links führt nicht zur Löschung der Ressource
- 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
- smart Links referenzieren das Knotenobjekt
- verschieben des Ressourceknotens direkt über die Datei oder indirekt über ein Verzeichnis invalidieren den smart link nicht
- sie sind Stellvertreter ihrer Ressource
- insbesondere smart Links müssen ihre Ressource beobachten
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.
Mittels
public class MySymbolicLink extends MyLink{
protected String getText(){
MyFileSystem.getInstance().getRoot().getNode( ressource ).getText();
}
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.
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());
}
}
}
Nutzen:
- Die Ressource Knoten wird nur einmal angelegt, aber öfters referenziert.
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:
- 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
- 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
statisch
Es gibt zwei Rollen im Beobachter Pattern, die des Beobachter und die des Subjekts, das sich beobachten lässt.- 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();
}
- 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;
}
Für die Aktualisierung des Beobachters bieten sich zwei Möglichkeiten an:
- push: das Subjekt übergibt die Daten beim benachrichtigen des Beobachter
- pull: das Subjekt meldet dem Beobachter nur die Veränderung und dieser holt sich dann die Daten vom Subjekt
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
- 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) );
---------------------- 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
- prüfen auf dead links und gegebenenfalls löschen des Links:
System.out.println("---------------------- remove all dead links");
myFileSystem.process( "", new CleanDeadLinksVisitor() );
---------------------- remove all dead links
removeNode Projekt.txt.symLink from dir C++/SpielWiese/Gui/Elkin/ABSOLUTELY/webNEW
- Auskunft über die konkreten Knotentypen
System.out.println("---------------------- count all node types: ");
myFileSystem.process("", new CountNodeTypesVisitor() );
---------------------- 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:- es existieren zwei Ableitungshierachie
- die Gastgeber, die den Besucher aufnehmen wollen und daher die Methode ==accept()==implementieren müssen
- der Besucher, der eine spezifische Operation auf jedem Gastgeber ausführen wollen und insofern jeden Gastgeber auf eine besondere Art besuchen 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 Gastgebermethodeaccept()
angestossen werden. public void accept( Visitor visitor ){
visitor.visit(this);
- der erste virtuelle Dispatch findet auf dem Visitor (visitor) Typ statt:
CleanUpVisitor
,CleanDeadLinksVisitor
oderCountNodeTypesVisitor
- der zweite virtuelle Dispatch findet auf dem Knoten Typ (this) statt :
MyFile
,MyDir
,MySymbolicLink
oderMySmartLink=
Konkretes Beispiel
- die Applikationen nutzt myFileSystem, um alle Knoten unterhalb von root("") mit dem Visitor CountNodeTypesVisitor zu besuchen
VisitormyFileSystem.process("", new CountNodeTypesVisitor() );
- die Filesystemabstraktion übergibt einerseits den Visitor der Knotenhierachie und frägt andererseits die Ergebnisse der Prozessierung ab
myRoot.getNode( dir ).accept(visitor);
visitor.apply();
- in der acceptMethode findet nur der zweifache virtuelle Dispatch in zwei Variationen statt
MyFile
,MySymbolikLink
undMySmartLink
verhalten sich wie oben beschriebenMyDir
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 );
}
- 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++;}
- die Methoden apply dient als Interfacevereinbarung, mit der man die Information des Visitors auswerten kann
- Besonderheiten:
- Double-Dispatch-Mechanismus:
- Für diese doppelte Virtualität hat sich der Name Double-Dispatch-Mechanismus etabliert.
- Internen Iterator:
- Die
accept()
Methode vonMyDir
propagiert den Methodenaufruf an alle Kinder und somit auch an alle Nachkommen 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.
- Die
- Double-Dispatch-Mechanismus:
- leichte Erweiterung der Hierachie um weitere Operationen
- Trennung der Traversierung von der Operation auf den Knote
Weiterlesen...