it-swarm.com.de

Refactoring viele andere if, else if, else if usw. Anweisungen

Ich versuche, lesbare Daten aus PDFs zu analysieren, und schreibe am Ende immer wieder Code wie folgt:

if (IsDob(line))
{
    dob = ParseDob(line);
}
else if (IsGender(line))
{
    gender = ParseGender(line);
}
...
...
...
else if (IsRefNumber(line))
{
    refNumber = ParseRefNumber(line);
}
else
{
    unknownLines.Add(line);
}

Dann verwende ich all diese Daten, um relevante Objekte aufzubauen, z. Kunden, die alle ihre persönlichen Daten verwenden.

Dies neigt dazu, etwas hässlich zu werden, wenn es viel zu analysieren gibt.

Ich habe sie in Funktionen wie TryParsePersonalInfo (Zeile), TryParseHistory (Zeile) usw. aufgeteilt. Aber das scheint das Problem nur zu verschieben, da ich immer noch diese endlosen anderen Wenns überall habe.

6
Levi H

Ich würde mit den von Ihnen angegebenen Informationen beginnen.

Erstellen Sie eine Schnittstelle wie folgt:

interface LineProcessor<E> {
  boolean Process(Record record, Line line); 
}

Nehmen wir an, Record ist vorerst eine richtige Tasche. Lass dich nicht aufhängen, ich halte es zu Demonstrationszwecken einfach.

class Record {
  public Date dob { get; set; }
  public String gender { get; set; }
  public String refNumber { get; set; }
  // ...
}

Entschuldigung, wenn die Syntax für C # nicht stimmt. Wenn Sie nicht sicher sind, worauf ich hinaus will, werde ich das klarstellen.

Erstellen Sie dann eine Liste der Instanzen von LineParser: eine für den Leitungstyp. Dann schreiben Sie eine Schleife (Python/Pseudocode):

for line in pdf:
  obj = process(record, line)

def process(record, line):
  for handler in processors:
    if handler.process(record, line): return
  else:
    unknown.add(line)

Nun könnte die Implementierung einer dieser Optionen folgendermaßen aussehen:

class DobProcessor implements LineProcessor {
  boolean process(Record record, Line line) {
    if (IsDob(line)) {
      record.dob = ParseDob(line);
      return true;
    } else {
      return false;
    }
  }

  Date ParseDob(Line line) {
    //...
  }

  boolean IsDob(Line line) {
    //...
  }
}

Dies sollte den Code übersichtlicher machen. Anstelle einer großen if-Anweisung haben Sie eine Reihe von Klassen, von denen jede für einen Fall spezifisch ist. Dies organisiert nicht nur den Code, sondern bedeutet auch, dass Sie Fälle hinzufügen oder entfernen können, ohne den Code in anderen Fällen zu berühren.

Eine andere Sache ist, dass Sie jetzt, da die Prozessschnittstelle auf eine einzige Methode beschränkt ist, dies tatsächlich eher als Funktionszeiger/Lambda betrachten können, sodass Sie auf die Schnittstelle verzichten können, wenn Sie dies wünschen.

10
JimmyJames

Verwenden Sie eine Liste von Delegierten

... um idemopotente Parsing-Funktionen aufzurufen, die in einer injizierbaren Klasse enthalten sind, die in einem extesnsible Objektmodell enthalten ist

Ich werde hier einige Programmierkonzepte vorstellen, also tragen Sie die lange Antwort. Dies scheint zu kompliziert, bis Sie daran gewöhnt sind. Aber all diese Muster sind tatsächlich sehr verbreitet und in kommerzieller Software sehr nützlich.

Erstellen Sie ein DTO

Zunächst müssen Sie Ihre Ergebnisse irgendwo zusammen speichern. Eine DTO-Klasse vielleicht; würde wie dieses Beispiel aussehen. Ich habe eine ToString() -Überschreibung hinzugefügt, damit Sie den Inhalt im Debug-Bereich anstelle nur des Klassennamens sehen können:

public enum Gender
{
    Male, Female
}

public class DocumentMetadata
{
    public DateTime DateOfBirth { get; set; }
    public Gender Gender { get; set; }
    public string RefNum { get; set; }

    public override string ToString()
    {
        return string.Format("DocumentMetadata: DOB={0:yyyy-MM-dd}, Gender={1}, RefNum={2}", DateOfBirth, Gender, RefNum);
    }
}

Definieren Sie einen Delegaten für Parser-Methoden, die einem Muster folgen

Nachdem wir nun ein DTO haben, können wir überlegen, wie die Zeilen analysiert werden sollen. Idealerweise möchten wir eine Reihe von idempotenten Funktionen, die Sie problemlos testen können. Und um sie zu wiederholen, wäre es hilfreich, wenn sie sich in irgendeiner Weise ähneln würden. Also definieren wir einen Delegierten:

public delegate bool Parser(string line, DocumentMetadata dto);

Wir können also eine Parser-Methode wie diese schreiben:

protected bool ParseDateOfBirth(string line, DocumentMetadata dto)
{
    if (!line.StartsWith("DOB:")) return false;
    dto.DateOfBirth = DateTime.Parse(line.Substring(4));
    return true;
}

Wir können eine beliebige Anzahl von Parser-Methoden schreiben. Solange sie einen Booleschen Wert zurückgeben und eine Zeichenfolge und ein DTO-Objekt als Argumente akzeptieren, stimmen sie alle mit dem Delegaten überein, sodass sie alle in eine Liste wie folgt eingefügt werden können:

List<Parser>  parsers = new List<Parser>
{
    ParseDateOfBirth,
    ParseGender,
    ParseRefNum
};

Wir werden diese Funktion in einem Moment verwenden, wenn wir die Parser-Klasse schreiben.

Erstellen Sie die Basisparserklasse

Hier sind noch ein paar Dinge, über die Sie sich Sorgen machen sollten:

  1. Wir möchten, dass unsere Parser-Methoden in einer einzelnen Codeeinheit enthalten sind, z. eine Klasse.
  2. Wir möchten, dass die Klasse injizierbar ist, z. Für den Fall, dass Sie in Zukunft mehr als eine Art Parser benötigen, können Sie diese für Komponententests herausnehmen.
  3. Wir möchten, dass die Logik für iterierende Parser allgemein ist.

Definieren Sie also zuerst eine Schnittstelle:

public interface IDocumentParser
{
    DocumentMetadata Parse(IEnumerable<string> input);
}

Ein abstrakter Basisparser:

public abstract class BaseParser : IDocumentParser
{
    protected abstract List<Parser> GetParsers();

    public virtual DocumentMetadata Parse(IEnumerable<string> input)
    {
        var parsers = this.GetParsers();
        var instance = new DocumentMetadata();

        foreach (var line in input)
        {
            foreach (var parser in parsers)
            {
                parser(line, instance);  //This is the line that does it all!!!
            }
        }
        return instance;
    }       
}

Oder wenn wir möchten, dass die Analysefunktion etwas cleverer ist (und auch die Zeilen zählt, die erfolgreich analysiert wurden):

    public virtual DocumentMetadata Parse(IEnumerable<string> input)
    {
        var parsers = this.GetParsers();
        var instance = new DocumentMetadata();

        var successCount = input.Sum( line => parsers.Count( parser => parser(line, instance) ));

        Console.WriteLine("{0} lines successfully parsed.", successCount);

        return instance;
    }

Während die LINQ-Lösung "cleverer" ist, können die verschachtelten Schleifen die Absicht klarer kommunizieren. Urteilsruf hier. Ich mag die LINQ-Version, weil ich die erfolgreichen Zeilen zählen und diese Informationen möglicherweise zur Validierung des Dokuments verwenden kann.

Implementieren Sie die Parser

Jetzt haben wir ein grundlegendes Framework zum Parsen von Dokumenten. Wir müssen lediglich GetParsers implementieren, damit eine Liste der Methoden zurückgegeben wird, die die Arbeit ausführen:

public class DocumentParser : BaseParser
{
    protected override List<Parser> GetParsers()
    {
        return new List<Parser>
        {
            ParseDateOfBirth,
            ParseGender,
            ParseRefNum
        };
    }

    private bool ParseDateOfBirth(string line, DocumentMetadata dto)
    {
       ///Implementation
    }

    private bool ParseGender(string line, DocumentMetadata dto)
    {
       ///Implementation        
    }

    private bool ParseRefNum(string line, DocumentMetadata dto)
    {
       ///Implementation
    }
}

Beachten Sie, dass in der endgültigen Implementierung nur die dokumentenspezifische Logik enthalten ist. Und alles, was es tut, ist die Delegierten über GetParsers() zu versorgen. Alle gängige Logik befindet sich in der Basisklasse, wo sie wiederverwendet werden kann.

Prüfung

Wir können jetzt ein Dokument mit ein paar Codezeilen analysieren:

var parser = new DocumentParser();
var doc = parse.Parse(input);

Aber wir möchten dieses Ding injizieren, also schreiben wir es richtig:

public class Application
{
    protected readonly IDocumentParser _parser; // injected

    public Application(IDocumentParser parser)
    {
        _parser = parser;
    }

    public void Run()
    {
        var input = new string[]
        {
            "DOB:1/1/2018",
            "Sex:Male",
            "RefNum:1234"
        };

        var result = _parser.Parse(input);

        Console.WriteLine(result);
    }
}

public class Program
{
    public static void Main()
    {
        var application = new Application(new DocumentParser());
        application.Run();
    }
}

Ausgabe:

3 lines successfully parsed.
DocumentMetadata: DOB=2018-01-01, Gender=Male, RefNum=1234

Jetzt haben wir alle folgenden:

  1. Generische Logik zum Durchlaufen aller Parser
  2. Ein erweiterbares Objektmodell, mit dem neue Parser eingeführt werden können
  3. Eine injizierbare Schnittstelle
  4. Demopotente, Unit-testbare Methoden, die das komplizierte Zeug erledigen
  5. Die Fähigkeit, erfolgreiche Analysevorgänge zu zählen (die beispielsweise verwendet werden können, um sicherzustellen, dass das Dokument gültig ist)
  6. Eine Klasse, die die resultierenden Daten kapselt

Arbeitsbeispiel für DotNetFiddle

2
John Wu

Beantworten Sie Ihre Frage

Eine C # -ähnlichere Lösung wäre, die Vorteile der Delegierten zu nutzen:

delegate bool TryParseHandler(string line);

private readonly TryParseHandler[] _handlers = new[]
{
    TryParseDob,
    TryParseGender,
    TryParseRefNumber,
    //...
}
bool TryParseDob(string line)
{
    if(!IsDob(line)) return false;
    dob = ParseDob(line);
    return true;
}
//etc
void ProcessLine(string line)
{
    foreach(var handler in _handlers)
    {
        if(handler(line)) return;
    }
}

Antworte auf dein Problem

Die richtige Antwort wäre, Ihre "Ist" -Lösung ganz aufzugeben. Wie sieht "Linie" aus? Ist es regelmäßig? Enthält es Schlüsselwörter? Sieht es zum Beispiel aus wie dob: 1/1/1990, gender: female, ref: 123456? Wenn ja, möchten Sie diese Schlüsselwörter herausziehen und für Ihre Suche verwenden:

private readonly IReadOnlyDictionary<string, Action<string>> _setValueLookup = new Dictionary<string, Action<string>>
{
    ["dob"] = s => dob = DateTime.Parse(s),
    ["gender"] = s => gender = ParseGender(s),
    ["ref"] = s => refString = s,
};
void ProcessLine(string line)
{
    var pair = line.Split(new []{':'}, 2);
    if(pair.Length < 2) 
    {
        unknownLines.Add(line);
        return;
    }

    var key = pair[0];
    var value = pair[1];
    if(!_setValueLookup.TryGetValue(key, out Action<string> callback))
    {
        unknownLines.Add(line);
        return;
    }

    callback(value);
}
1
Kevin Fee