web-dev-qa-db-fra.com

Refactorisation de nombreuses instructions if, else if, else if, etc.

J'essaie d'analyser des données lisibles à partir de fichiers PDF et je continue d'écrire du code comme celui-ci:

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

Ensuite, j'utilise toutes ces données pour construire des objets pertinents, par exemple Client utilisant toutes ses données personnelles.

Cela a tendance à devenir un peu moche quand il y a beaucoup à analyser.

Je les ai divisés en fonctions comme TryParsePersonalInfo (ligne), TryParseHistory (ligne), etc.

6
Levi H

Voici ce que je commencerais avec les informations que vous avez fournies.

Créez une interface comme celle-ci:

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

Supposons que Record soit un sac pour le moment. Ne vous y attardez pas, je reste simple à des fins de démonstration.

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

Désolé si la syntaxe n'est pas correcte pour C #. Si vous n'êtes pas sûr de ce que je veux en venir, je vais clarifier.

Créez ensuite une liste d'instances de LineParser: une pour le type de ligne. Ensuite, vous écrivez une boucle (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)

Maintenant, l'implémentation de l'un d'eux pourrait ressembler à ceci:

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

Cela devrait rendre le code plus facile à gérer. Au lieu d'une grande instruction if, vous aurez un certain nombre de classes où chacune est spécifique à un cas. Cela organise non seulement le code, cela signifie que vous pouvez ajouter ou supprimer des cas sans toucher au code autour d'autres cas.

Une autre chose est que maintenant que l'interface des processus est réduite à une seule méthode, vous pouvez réellement commencer à penser à cela comme davantage un pointeur de fonction/lambda afin de pouvoir vous dispenser de l'interface si vous le souhaitez.

10
JimmyJames

Utilisez une liste de délégués

... pour appeler des fonctions d'analyse idemopotentes encapsulées dans une classe injectable contenue dans un modèle objet extensible

Je vais présenter quelques concepts de programmation ici, donc restez avec la réponse longue. Cela peut sembler trop compliqué jusqu'à ce que vous y soyez habitué. Mais tous ces modèles sont en fait très courants et très utiles dans les logiciels commerciaux.

Créer un DTO

Tout d'abord, vous avez besoin d'un endroit pour stocker tous vos résultats ensemble. Une classe DTO peut-être; ressemblerait à cet exemple. J'ai ajouté une substitution ToString() pour que vous puissiez voir le contenu dans le volet de débogage au lieu du seul nom de 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);
    }
}

Définissez un délégué pour les méthodes d'analyse qui suivent un modèle

Maintenant que nous avons un DTO, nous pouvons raisonner sur la façon d'analyser les lignes. Idéalement, nous voudrions une série de fonctions idempotentes que vous pouvez tester facilement. Et pour les parcourir, il serait utile qu'ils soient similaires d'une manière ou d'une autre. Nous définissons donc un délégué:

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

Nous pouvons donc écrire une méthode d'analyse similaire à ceci:

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

Nous pouvons écrire n'importe quel nombre de méthodes d'analyseur, et tant qu'elles renvoient un booléen et acceptent une chaîne et un objet DTO comme arguments, elles correspondront toutes au délégué, afin qu'elles puissent toutes être mises dans une liste, un peu comme ceci:

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

Nous utiliserons cette capacité à un moment où nous écrirons la classe d'analyseur.

Créer la classe d'analyseur de base

Voici maintenant quelques autres choses dont vous devez vous soucier:

  1. Nous voulons que nos méthodes d'analyse soient contenues dans une seule unité de code, par exemple une classe.
  2. Nous voulons que la classe soit injectable, par exemple au cas où vous auriez besoin de plus d'un type d'analyseur à l'avenir, et vous pouvez donc le supprimer pour des tests unitaires.
  3. Nous voulons que la logique d'itération des analyseurs soit commune.

Définissez donc d'abord une interface:

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

Un analyseur de base abstrait:

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

Ou si nous voulons que la fonction Parse soit un peu plus intelligente (et comptons également les lignes qui ont été analysées avec succès):

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

Alors que la solution LINQ est plus "intelligente", les boucles imbriquées peuvent communiquer l'intention plus clairement. Appel de jugement ici. J'aime la version LINQ car je peux compter les lignes qui réussissent, et éventuellement utiliser ces informations pour valider le document.

Implémenter les analyseurs

Nous avons maintenant un cadre de base pour l'analyse des documents. Tout ce dont nous avons besoin est d'implémenter GetParsers pour qu'il retourne une liste de méthodes qui font le travail:

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

Notez que seule la logique spécifique au document est conservée dans l'implémentation finale. Et il ne fait qu'alimenter les délégués via GetParsers(). Toute logique commune se trouve dans la classe de base, où elle peut être réutilisée.

Tester

Nous pouvons maintenant analyser un document avec quelques lignes de code:

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

Mais nous aimerions injecter cette chose, alors écrivons-la correctement:

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

Production:

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

Maintenant, nous avons tous les éléments suivants:

  1. Logique générique pour itérer sur tous les analyseurs
  2. Un modèle objet extensible permettant l'introduction de nouveaux analyseurs
  3. Une interface injectable
  4. Méthodes idemopotentes, testables par unité, qui font les choses compliquées
  5. La capacité de compter les opérations d'analyse réussies (qui pourraient être utilisées, par exemple, pour garantir la validité du document)
  6. Une classe qui encapsule les données résultantes

Exemple de travail sur DotNetFiddle

2
John Wu

Répondre à votre question

Une solution plus en C # serait de profiter des délégués:

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

Répondre à votre problème

La bonne réponse serait d'abandonner entièrement votre solution "est". À quoi ressemble la "ligne"? Est-ce régulier? Contient-il des mots clés? Par exemple, ressemble-t-il à dob: 1/1/1990, gender: female, ref: 123456? Si oui, vous souhaitez extraire ces mots clés et les utiliser pour votre recherche:

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