Refactoring di molte altre istruzioni if, else if, else if, etc.

5

Sto cercando di analizzare i dati leggibili dai PDF e continuo a scrivere codice simile al seguente:

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

Quindi uso tutti questi dati per creare oggetti pertinenti, ad es. Cliente che utilizza tutti i propri dati personali.

Questo tende ad essere un po 'brutto quando c'è molto da analizzare.

Li ho divisi in funzioni come TryParsePersonalInfo (riga), TryParseHistory (linea) ecc. Ma sembra che si stia semplicemente spostando il problema perché ho ancora questi infiniti se c'è ovunque.

    
posta Levi H 03.05.2018 - 16:59
fonte

4 risposte

9

Ecco cosa vorrei iniziare date le informazioni che hai fornito.

Crea un'interfaccia in questo modo:

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

Supponiamo che Record sia una borsa adatta per ora. Non lasciarti coinvolgere da questo, lo terrò semplice a scopo dimostrativo.

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

Scusa se la sintassi non è corretta per C #. Se non sei sicuro di quello che sto ricevendo, ti chiarirò.

Quindi creare un elenco di istanze di LineParser: una per il tipo di linea. Quindi, scrivi un loop (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)

Ora l'implementazione di uno di questi potrebbe sembrare così:

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) {
    //...
  }
}

Questo dovrebbe rendere il codice più gestibile. Invece di una dichiarazione if grande, avrai un numero di classi in cui ognuna è specifica per un caso. Questo non solo organizza il codice, significa che puoi aggiungere o rimuovere casi senza toccare il codice in altri casi.

Un'altra cosa è che ora che l'interfaccia dei processi è ridotta a un singolo metodo, in realtà puoi iniziare a pensare a questo come a un puntatore / lambda di funzione in modo da poter fare a meno dell'interfaccia, se lo desideri.

    
risposta data 03.05.2018 - 17:18
fonte
1

Rispondi alla tua domanda

Un'altra soluzione C # -esca sarebbe quella di avvantaggiarsi dei delegati:

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;
    }
}

Rispondi al tuo problema

La risposta corretta sarebbe abbandonare del tutto la soluzione "è". Che aspetto ha "linea"? È regolare? Contiene parole chiave? Ad esempio, sembra dob: 1/1/1990 , gender: female , ref: 123456 ? In tal caso, vuoi estrarre quelle parole chiave e utilizzarle per la tua ricerca:

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);
}
    
risposta data 03.05.2018 - 17:54
fonte
1

Utilizza un elenco di delegati

... per chiamare le funzioni di analisi idemopotent incapsulate in una classe iniettabile contenuta in un modello di oggetto estensibile

Ho intenzione di introdurre alcuni concetti di programmazione qui così da sopportare la lunga risposta. Questo può sembrare complicato fino a quando non ci si abitua. Ma tutti questi pattern sono in realtà molto comuni e molto utili nel software commerciale.

Crea un DTO

In primo luogo, è necessario un posto dove archiviare i risultati tutti insieme. Una classe DTO forse; sembrerebbe questo esempio Ho aggiunto un override ToString() in modo da poter vedere i contenuti nel riquadro di debug invece del solo nome della classe:

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);
    }
}

Definisci un delegato per i metodi parser che seguono uno schema

Ora che abbiamo un DTO, possiamo ragionare su come analizzare le linee. Idealmente, vorremmo una serie di funzioni idempotent che è possibile testare facilmente. E per iterare su di loro, sarebbe utile se fossero simili in qualche modo. Quindi definiamo un delegato:

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

Quindi possiamo scrivere un metodo parser come questo:

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

Possiamo scrivere qualsiasi numero di metodi parser, e fintanto che restituiscono un booleano e accettano una stringa e un oggetto DTO come argomenti, corrisponderanno tutti al delegato, in modo che possano essere tutti inseriti in una lista, una sorta di così:

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

Utilizzeremo questa funzionalità in un momento in cui scriviamo la classe parser.

Crea la classe parser di base

Ora ci sono alcune altre cose di cui preoccuparsi:

  1. Vogliamo che i nostri metodi parser siano contenuti in una singola unità di codice, ad es. una classe.
  2. Vogliamo che la classe sia iniettabile, ad es. nel caso in cui sia necessario disporre di più di un tipo di parser in futuro, e quindi è possibile stub per i test unitari.
  3. Vogliamo che la logica per l'iterazione dei parser sia comune.

Quindi, prima definisci un'interfaccia:

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

Un parser di base astratto:

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;
    }       
}

O se vogliamo che la funzione Parse sia un po 'più intelligente (e contiamo anche le righe che sono state analizzate correttamente):

    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;
    }

Sebbene la soluzione LINQ sia più "intelligente", i loop nidificati possono comunicare più chiaramente l'intento. Chiamata di giudizio qui. Mi piace la versione LINQ perché posso contare le righe che riescono e possibilmente usare quelle informazioni per convalidare il documento.

Implementa i parser

Ora abbiamo una struttura di base per l'analisi dei documenti. Tutto ciò di cui abbiamo bisogno è implementare GetParsers in modo che restituisca un elenco di metodi che funzionano:

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

Si noti che solo la logica specifica del documento viene mantenuta nell'implementazione finale. E tutto ciò che fa è fornire i delegati tramite GetParsers() . Tutta la logica comune è nella classe base, dove può essere riutilizzata.

test

Ora possiamo analizzare un documento con un paio di linee di codice:

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

Ma ci piacerebbe iniettare questa cosa, quindi scriviamola correttamente:

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();
    }
}

Output:

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

Ora abbiamo tutto quanto segue:

  1. Logica generica per iterare su tutti i parser
  2. Un modello di oggetto estensibile che consente di introdurre nuovi parser
  3. Un'interfaccia iniettabile
  4. Metodi idemopotenziari testabili che fanno le cose complicate
  5. La possibilità di contare le operazioni di analisi di successo (che potrebbero essere utilizzate, ad esempio, per garantire che il documento sia valido)
  6. Una classe che incapsula i dati risultanti

Esempio di lavoro su DotNetFiddle

    
risposta data 03.05.2018 - 20:16
fonte
-1

Se sei felice di usare lambdas e linq, puoi semplificare tutto il codice come:

var lineHandlers = new List<(Func<LineType, bool> test, Action<LineType> action)>
{
    (IsDob, l => dob = ParseDob()),
    (IsGender, l => gender = ParseGender(l)),
    (IsRefNumber, l => refNumber = ParseRefNumber(l)),
    (_ => true, l => unknownLines.Add(l))
};

var (_, action) = lineHandlers.First(handler => handler.test(line));
action(line);

(Nota, richiede C # 7 per utilizzare le tuple. Sostituisci semplicemente quella tupla con un tipo semplice se stai usando una vecchia versione del compilatore).

    
risposta data 03.05.2018 - 19:19
fonte

Leggi altre domande sui tag