Refaktorierung dgrep

Aus Info-Theke
Zur Navigation springen Zur Suche springen

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

Der Quellcode kann dgrep.v1.zip hier heruntergeladen werden. Danach 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 vom verboseLevel eine Fehlermeldung ausgegeben.
  • try { ... } on ExitException catch (exc) In searchFile wird in Abhängikeit 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 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 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 passiert das auf einen Schlag. Bei Unittests darf das nicht gemacht werden, da sonst die Methode execute() kein Ergebnis liefert, das überprüft werden kann.