views:

102

answers:

2

I have a maintenance application that has to turn enterprise data (from various databases/tables) into flat files, each in a specific format, for consumption by a legacy application. I've got data models like

public class StatusCode
{
    public String Id { get; set; }
    public Char Level { get; set; }
    public String Description { get; set; }
}

I will select some subset or all of these records from the data source. And I need to map each entity to one line of the file, which may require adjusting the data (padding, transforming, or handling null).

public delegate String MapEntity<T>(T entity);
public MapEntity<StatusCode> MapStatusCode = delegate(StatusCode entity)
{
    return String.Format("{0},{1},{2}",
        entity.Id.PadLeft(3, '0'),
        entity.Level == 'S' ? 0 : 1,
        entity.Description ?? "-");
}

The question is, how do I write the transformation classes? Do I provide a "DefaultFileCreator" that takes a mapping delegate?

public interface IFileCreator
{
    Byte[] Create<T>(MapEntity<T> map, IEnumerable<T> entities);
}

public class DefaultFileCreator : IFileCreator
{
    public Byte[] Create<T>(MapEntity<T> map, IEnumerable<T> entities)
    {
        StringBuilder sb = new StringBuilder();
        foreach (T entity in entities)
            sb.AppendLine(map(entity));

        return Encoding.Default.GetBytes(sb.ToString());
    }
}

...
fileCreator.Create(MapStatusCode, repository<StatusCode>.FindAll());
...

With this solution, I'm concerned about where and in what scope I should keep the mapping delegates. And how I'm going to call them without knowing T (if I ever need to).

Or, do I change the interface and require the mapping in the concrete classes?

public interface IFileCreator<T>
{
    Byte[] Create(IEnumerable<T> entities);
}

public abstract class FileCreator : IFileCreator<T>
{
    protected abstract String Map(T entity);

    public Byte[] Create(IEnumerable<T> entities)
    {
        StringBuilder sb = new StringBuilder();
        foreach (T entity in entities)
            sb.AppendLine(Map(entity));

        return Encoding.Default.GetBytes(sb.ToString());
    }
}

public class StatusCodeFile : FileCreator<StatusCode>
{
    public override String Map(T entity)
    {
        return String.Format("{0},{1},{2}",
            entity.Id.PadLeft(3, '0'),
            entity.Level == 'S' ? 0 : 1,
            entity.Description ?? "-");
    }
}

This solution explodes in concrete classes, but they're as thin as the mapping delegates. And I feel more comfortable working with IFileCreator<T> and a factory. (again, only if necessary).

I'm assuming some base class is useful as the StringBuilder loop and Byte[] encoding are straightforward. Should the concrete class set a delegate property in the base class (rather than calling an abstract method)? Should I keep the type parameter on the method (and how would that affect the base/concrete classes)?

I'm up for any solution. My main goal is ease of maintenance. I have 12 models/files right now and this may increase up to 21. I may have a requirement to insert arbitrary header/footer lines in any file (which is why I like the overrideable base class method, Map).

+1  A: 

Do you have to actually create concrete subclasses for each possible mapping? Maybe it would be possible to instead use an XML file (or a database) to describe the format/contents of each kind of file. You then have a single class that takes a "FileType" key and uses the formatting information from the XML to determine how to build the file for that FileType.

Eric Petroelje
I think this is the same solution (in intent) as using the mapping delegate.
Anthony Mastrean
@ajmastrean - now that I look at it more closely, you are right. As for how you would do this without knowing what "T" is - you would just be using reflection to get all the properties, so you really don't need to know the type of the object anyways.
Eric Petroelje
A: 

Now that I've written a few of the transformations, I'm leaning towards the mapping-per-class approach,. I have to be able to run unit tests against faked models to ensure the files are built properly (testing them against known good sample files).

I've been making the mapping-delegates private to the "workflow" class they belong in (one "workflow" per model/file). I'd have to make them public to unit test them, or output the intermediate file content (the workflow saves the completed files to a data store).

The mapping-per-class seems a lot more testable and decomposable.

Anthony Mastrean
I'm also considering overriding an abstract property in each concrete class, MapEntity (delegate). Same effect as overriding a method Map(T). Different look.
Anthony Mastrean