Refaktorierung dgrep - flutter-tutorial-de/dart-programming GitHub Wiki
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 genug.
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, der für eine unkomplizierte weitere Verwendung in einer eigenen Klasse implementiert wird, 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").
Da ein Generator normalerweise Optionen und Zustände hat, wird er üblicherweise in eine Klasse eingebettet.
Der Generator selbst 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, an denen 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 durch den nächsten Aufruf der Methode der Ablauf genau an dieser Stelle fortgesetzt wird.
Daher können beliebige Algorithmen benutzt werden, die bei jedem Aufruf lediglich 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 evtl. viel zu viele Dateien gesammelt werden: Mit der Option --exit-files bricht das Programm ab, wenn "genügend" Dateien mit Treffern gefunden sind. Mit Generator wird eine Anweisung sofort nach Eintreffen der Bedingung ausgeführt, bei der alternativen Methode erst nach dem langwierigen Sammeln aller Dateien!
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.
Eine Diskussion der Klasse mit dem Generator findet in Datei file_supplier.dart statt.
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 den 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.
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 initialisiert. -
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ängigkeit 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, wenn nötig, auf drei Stellen auf. -
'= 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
) sowie 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 dagegen passiert das auf einen Schlag. Bei Unittests darf das nicht gemacht werden, da sonst die Methodeexecute()
kein Ergebnis liefern würde, das überprüft werden könnte.