Use a list of delegates
...to call idemopotent parsing functions encapsulated in an injectable class contained in an extesnsible object model
I'm going to introduce a few programming concepts here so bear with the long answer. This may seem overcomplicated until you're used to it. But all of these patterns are actually very common and very useful in commercial software.
Create a DTO
First, you need somewhere to store your results all together. A DTO class perhaps; would look like this example. I added a ToString()
override so you can see the contents in the debug pane instead of just the class name:
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);
}
}
Define a delegate for parser methods that follow a pattern
Now that we have a DTO, we can reason about how to parse the lines. Ideally, we'd want a series of idempotent functions that you can unit test easily. And to iterate over them, it would be helpful if they were similar in some way. So we define a delegate:
public delegate bool Parser(string line, DocumentMetadata dto);
So we can write a parser method sort of like this:
protected bool ParseDateOfBirth(string line, DocumentMetadata dto)
{
if (!line.StartsWith("DOB:")) return false;
dto.DateOfBirth = DateTime.Parse(line.Substring(4));
return true;
}
We can write any number of parser methods, and as long as they return a boolean and accept a string and a DTO object as arguments, they will all match the delegate, so they could all be put into a list, sort of like this:
List<Parser> parsers = new List<Parser>
{
ParseDateOfBirth,
ParseGender,
ParseRefNum
};
We'll use this capability in a moment when we write the parser class.
Create the base parser class
Now here are a few more things to worry about:
- We want our parser methods contained in a single unit of code, e.g. a class.
- We want the class to be injectable, e.g. in case you need to have more than one kind of parser in the future, and so you can stub it out for unit tests.
- We want the logic for iterating parsers to be common.
So first define an interface:
public interface IDocumentParser
{
DocumentMetadata Parse(IEnumerable<string> input);
}
An abstract base parser:
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;
}
}
Or if we want the Parse function to be a bit more clever (and also count the lines that are successfully parsed):
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;
}
While the LINQ solution is more "clever" the nested loops may communicate the intent more clearly. Judgment call here. I like the LINQ version because I can count the lines that succeed, and possibly use that information to validate the document.
Implement the parsers
Now we have a basic framework for parsing documents. All we need is to implement GetParsers
so that it returns a list of methods that do the work:
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
}
}
Notice that only the document-specific logic is held in the final implementation. And all it does is supply the delegates via GetParsers()
. All common logic is in the base class, where it can be reused.
Test
We can now parse a document with a couple lines of code:
var parser = new DocumentParser();
var doc = parse.Parse(input);
But we'd like to inject this thing, so let's write it properly:
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
Now we have all of the following:
- Generic logic for iterating over all the parsers
- An extensible object model allowing new parsers to be introduced
- An injectable interface
- Idemopotent, unit-testable methods that do the complicated stuff
- The ability to count successful parse operations (which could be used, for example, to ensure the document is valid)
- A class that encapsulates the resulting data
Working example on DotNetFiddle