Refaktorierung dgrep
Zielsetzung
Das Programm Projekt dgrep hat eine mächtige Möglichkeit eingebaut, Dateien in einem Dateibaum zu finden.
Es sind viele weitere Programme denkbar, die diese Funktionalität ebenfalls benötigen, beispielsweise ein Backupprogramm.
Die Dateisuche findet in der Methode searchFilePatterns()
statt.
Die "Nutzfunktion", also die Suche in der Datei (Methode searchFile
)
wird in searchFilePatterns()
aufgerufen.
Damit ist ein Herauslösen der Methode schwierig. Eine Callback-Funktion wäre eine Lösung, ist aber nicht flexibel.
Besser ist ein Objekt, das mit den zu findenden Eigenschaften der Datei "gefüttert" wird und einfach nacheinander die Dateinamen liefert.
Dart bietet dafür tatsächlich eine Lösung, einen Generator.
Wir gestalten also das Projekt so um, dass ein Generator, implementiert in einer eigenen Klasse für unkomplizierte weitere Verwendung, die Dateisuche erledigt.
Da die Funktionalität des Programms nicht verändert wird, bleiben die Tests gültig und können den Erhalt der Funktionalität ohne weiteren Aufwand nachweisen. Das nennt man einen Regressionstest ("Rückschrittstest").
Das Konzept eines Generators
Da ein Generator normalerweise Optionen und Zustände hat, wird er üblicherweise in eine Klasse eingebettet.
Der Generator selber ist dann eine Methode, markiert mit sync*
, wenn
der Generator rekursiv ist, ansonsten mit sync
.
Die Methode realisiert wie üblich einen passenden Algorithmus, an den Stellen, wo das Ergebnis (in unserem Fall ein Dateiname) berechnet ist, wird dieser mit yield "abgeliefert".
In unserem Fall wird rekursiv der Dateibaum durchlaufen, wenn die Bedingungen erfüllt sind, wird die yield-Anweisung ausgeführt. Die Methode bleibt dann "an dieser Stelle" stehen, bis der nächste Aufruf der Methode der Ablauf genau an dieser Stelle fortgesetzt wird.
Daher können beliebige Algorithmen benutzt werden, die bei jedem Aufruf nur bis zum nächsten Treffer weiterlaufen.
Die Alternative wäre, alle Dateien in einer Liste aufzusammeln und dann die Liste abzuarbeiten. Das hat den Nachteil, dass eine lange Pause entsteht, und dass evt. viel zu viele Dateien gesammelt werden: Mit der Option --exit-files bricht das Programm ab, wenn "genügend" Dateien mit Treffern gefunden ist. Mit Generator sofort nach Eintreffen der Bedingung, wenn vorher aber die Dateien aufgesammelt werden, erst nach dem langdauerenden Sammeln aller Dateien.
Der Quellcode
Bitte die Datei dgrep.v2.zip herunterladen und die Zipdatei im Projektverzeichnis von dgrep (~/dev/dgrep oder c:\dev\dgrep) entpacken. Dabei müssen die vorhandenen Dateien überschrieben werden.
Die Klasse FileSupplier
Eine Diskussion der Klasse mit dem Generator findet in Datei file_supplier.dart statt.
Änderungen in FileOptions
Die Dateioptionen sind naturgemäß in die Datei file_supplier.dart gewandert.
Unser neuer Generator soll etwas allgemeiner nutzbar sein, daher kann er mehr, als die Textsuche benötigt. Diese Änderungen spiegeln sich in Suchoptionen wieder: Es kommen folgende Attribute dazu:
bool yieldFile = true; bool yieldDirectory = true; bool yieldLinkToFile = true; bool yieldLinkToDirectory = true;
Es kann damit festgelegt werden, welche Art von Dateien geliefert wird:
- Verzeichnisse
- Links, die auf Verzeichnisse zeigen
- Links, die auf Dateien zeigen
- Sonstige Dateien.
Umbau in der Klasse SearchEngine
Es muss nur die Methode search
geändert werden, die damit übersichtlicher wird:
// Searches the files using the FileSupplier class and applies the text search. void search() { String exitMessage; try { final pattern2 = searchOptions.word ? '\\b$pattern\\b' : pattern; regExp = RegExp(pattern2, caseSensitive: !searchOptions.ignoreCase); fileOptions.yieldDirectory = false; fileOptions.yieldLinkToDirectory = false; final supplier = FileSupplier( fileOptions: fileOptions, filePatterns: filePatterns, verboseLevel: verboseLevel); try { for (var filename in supplier.next()) { if (!searchFile(filename)) { supplier.ignoredFiles++; supplier.processedFiles--; if (verboseLevel >= 4) { print('= ignored because of access: $filename'); } } } } on ExitException catch (exc) { exitMessage = '= search stopped: ' + exc.reason; } if (verboseLevel >= 1) { var hits = searchOptions.count || searchOptions.list ? '' : ' matching lines: $totalHitLines'; print(supplier.summary[0] + hits); print(supplier.summary.sublist(1).join('\n')); final diff = DateTime.now().difference(startTime); final msec = (diff.inMilliseconds % 1000).toString().padLeft(3); print( '= runtime: ${diff.inHours}h${diff.inMinutes % 60}m${diff.inSeconds % 60}.$msec'); if (exitMessage != null) { print(exitMessage); } } } on FormatException catch (exc) { usage('error in regular expression "$pattern": $exc'); } if (!SearchEngine.storeResult) { // Exit at once: Perhaps no garbage collection. exit(0); } }
pattern2 = searchOptions.word ? '\\b$pattern\\b' : pattern;
Wenn die Option--word
gesetzt ist, wird das Suchmuster ergänzt.fileOptions.yieldDirectory = false;
Standardmäßig werden alle Arten von Dateien geliefert, wir brauchen aber keine Verzeichnisnamen.supplier = FileSupplier(fileOptions: fileOptions, ...);
Der Generator wird initialiert.for (var filename in supplier.next())
In dieser Schleife wird über die Werte des Generators iteriert.if (!searchFile(filename))
Wenn die Dateisuche mit Fehler beendet wurde, wird die Statistik angepasst und abhängig vomverboseLevel
eine Fehlermeldung ausgegeben.try { ... } on ExitException catch (exc)
InsearchFile
wird in Abhängikeit von den Optionen--exit-lines
oder--exit-files
der Schnellaustieg mittels der AusnahmeExitException
getätigt.print(supplier.summary[0] + hits);
DasFileSupplier
-Objekt bereitet die Statistik in der Stringlistesummary
auf, die erste wird noch mit der Trefferzahl ergänzt.diff = DateTime.now().difference(startTime);
- Das (neue) Attribut
startTime
der KlasseSearchEngine
wird mitfinal startTime = DateTime.now();
initialisiert. - Wir ermitteln die aktuelle Zeit mit
DateTime.now()
, das ergibt ein Objekt der KlasseDateTime
. - Die Methode
difference()
ermittelt die Differenz der aufrufenden Instanz mit einem andernDateTime
-Objekt, hierstartTime
, Ergebnis ist ein Objekt vom TypeDuration
.
- Das (neue) Attribut
msec = (diff.inMilliseconds % 1000).toString().padLeft(3);
Wir interessieren uns nur für den Rest der Millisekunden bis zu einer Sekunde, wandeln das in einen String und füllen auf 3 Stellen auf, wenn nötig.'= runtime: ${diff.inHours}h${diff.inMinutes % 60}m${diff.inSeconds % 60}.$msec'
Es wird eine gut lesbare Formatierung des Zeitunterschieds aufbereitet: Stunden, den Rest der Minuten (inMinutes % 60
), den Rest der Sekunden (inSeconds % 60
), beispielsweise "0h12m32.012".if (!SearchEngine.storeResult)
Wenn kein Unittest vorliegt, wird das Programm sofort mitexit(0)
beendet. Das verkürzt die Laufzeit bei vielen Dateien im Suchbaum erheblich: Sonst müssen Millionen von Strings freigegeben werden, mit der Abkürzung passiert das auf einen Schlag. Bei Unittests darf das nicht gemacht werden, da sonst die Methodeexecute()
kein Ergebnis liefert, das überprüft werden kann.