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:
- Vogliamo che i nostri metodi parser siano contenuti in una singola unità di codice, ad es. una classe.
- 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.
- 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:
- Logica generica per iterare su tutti i parser
- Un modello di oggetto estensibile che consente di introdurre nuovi parser
- Un'interfaccia iniettabile
- Metodi idemopotenziari testabili che fanno le cose complicate
- La possibilità di contare le operazioni di analisi di successo (che potrebbero essere utilizzate, ad esempio, per garantire che il documento sia valido)
- Una classe che incapsula i dati risultanti
Esempio di lavoro su DotNetFiddle