Refaktorierung dgrep - flutter-tutorial-de/dart-programming GitHub Wiki

Table of Contents

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 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").

Das Konzept eines Generators

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!

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 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.

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 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 vom verboseLevel eine Fehlermeldung ausgegeben.
  • try { ... } on ExitException catch (exc) In searchFile wird in Abhängigkeit von den Optionen --exit-lines oder --exit-files der Schnellaustieg mittels der Ausnahme ExitException getätigt.
  • print(supplier.summary[0] + hits); Das FileSupplier-Objekt bereitet die Statistik in der Stringliste summary auf, die erste wird noch mit der Trefferzahl ergänzt.
  • diff = DateTime.now().difference(startTime);
    • Das (neue) Attribut startTime der Klasse SearchEngine wird mit final startTime = DateTime.now(); initialisiert.
    • Wir ermitteln die aktuelle Zeit mit DateTime.now(), das ergibt ein Objekt der Klasse DateTime.
    • Die Methode difference() ermittelt die Differenz der aufrufenden Instanz mit einem andern DateTime-Objekt, hier startTime, Ergebnis ist ein Objekt vom Type Duration.
  • 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 mit exit(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 Methode execute() kein Ergebnis liefern würde, das überprüft werden könnte.
⚠️ **GitHub.com Fallback** ⚠️