11

I'm looking for patterns or architectural guidance for an upcoming feature I'm designing. Basically, it's an export feature with multiple export targets, and I'm looking to find a way to make it generic enough where plugging in new export targets don't require a lot of core changes. By export targets, I'm simply referring to different types of output, whether that be PDFs, PowerPoint presentations, Word documents, RSS, etc. I have a base set of data, which is represented in JSON and XML. This data are used to construct images (using any number or export types [e.g., PNG, JPG, GIF, etc), graphs, textual representations, tables, and more.

I'm trying to find a way to abstract all the rendering and layout into some kind of rendering or layout engine that handles the addition of further export targets. Any help/suggestions/resources as to how to approach this would be greatly appreciated. Thanks in advance.

For a pictorial representation of what I'm trying to achieve.

enter image description here

3
  • Can you describe what you have tried so far? What are the requirements (responsibilities) of the layout engine? For example, is it expected to handle pagination and page size selection?
    – rwong
    Commented Jun 6, 2013 at 2:39
  • Can the XML/JSON data be used to create multiple output types on the same output run, i.e. your XML data produces Images and Tables and Graphs in a PDF document? Or can the XML/JSON data only be used to create a Table or a Graph for a PDF document?
    – Gibson
    Commented Jun 6, 2013 at 10:48
  • This is all about xkcd.com/927 - why are you trying to reinvent the wheel? DocBook, Markdown/pandoc etc. already exist... Commented Jun 17, 2013 at 18:07

5 Answers 5

2

For me, the way to go would be interfaces and a Factory. One that returns references to interfaces behind which various classes can hide. The classes that do the actual grunt work all need to be registered with the Factory so it knows which class to instantiate given a set of parameters.

Note: instead of interfaces you could also use abstract base classes, but the drawback there is that for single inheritance languages it limits you to a single base class.

TRepresentationType = (rtImage, rtTable, rtGraph, ...);

Factory.RegisterReader(TJSONReader, 'json');
Factory.RegisterReader(TXMLReader, 'xml');

Factory.RegisterWriter(TPDFWriter, 'pdf');
Factory.RegisterWriter(TPowerPointWriter, 'ppt');
Factory.RegisterWriter(TWordWriter, 'doc');
Factory.RegisterWriter(TWordWriter, 'docx');

Factory.RegisterRepresentation(TPNGImage, rtImage, 'png');
Factory.RegisterRepresentation(TGIFImage, rtImage, 'gif');
Factory.RegisterRepresentation(TJPGImage, rtImage, 'jpg');
Factory.RegisterRepresentation(TCsvTable, rtTable, 'csv');
Factory.RegisterRepresentation(THTMLTable, rtTable, 'html');
Factory.RegisterRepresentation(TBarChart, rtTGraph, 'bar');
Factory.RegisterRepresentation(TPieChart, rtTGraph, 'pie');

Code is in Delphi (Pascal) syntax as that is the language with which I am most familiar.

After all implementing classes are registered with the factory, you should be able to request an interface reference to an instance of such a class. For example:

Factory.GetReader('SomeFileName.xml');
Factory.GetWriter('SomeExportFileName.ppt');
Factory.GetRepresentation(rtTable, 'html');

should return an IReader reference to an instance of TXMLReader; an IWriter reference to an instance of TPowerPointWriter and an IRepresentation reference to an instance of THTMLTable.

Now all the rendering engine needs to do, is tie everything together:

procedure Render(
  aDataFile: string; 
  aExportFile: string;
  aRepresentationType: TRepresentationType;
  aFormat: string;
  );
var
  Reader: IReader;
  Writer: IWriter;
  Representation: IRepresentation;
begin
  Reader := Factory.GetReaderFor(aDataFile);
  Writer := Factory.GetWriterFor(aExportFile);
  Representation := Factory.GetRepresentationFor(aRepresentationType, aFormat);

  Representation.ConstructFrom(Reader);
  Writer.SaveToFile(Representation);
end;

The IReader interface should provide methods to read the data needed by IRepresentation implementers to construct the representation of the data. Similarly IRepresentation should provide methods that IWriter implementers need to export the data representation to the requested export file format.

Assuming the data in your files is tabular in nature, IReader and its supporting interfaces could look like:

IReader = interface(IInterface)
  function MoveNext: Boolean;
  function GetCurrent: IRow;
end;

IRow = interface(IInterface)
  function MoveNext: Boolean;
  function GetCurrent: ICol;
end;

ICol = interface(IInterface)
  function GetName: string;
  function GetValue: Variant;
end;

Iterating over a table would then be a matter of

while Reader.MoveNext do
begin
  Row := Reader.GetCurrent;
  while Row.MoveNext do
  begin
    Col := Row.GetCurrent;
    // Do something with the column's name or value
  end;
end;

As the representations can be images, graphs and textual in nature, IRepresentation would probably have similar methods to IReader to traverse a constructed table and it would have methods to get the images and graphs, for example as a stream of bytes. It would be up to the IWriter implementers to encode the table values and the image/graph bytes as required by the export target.

1

While I agree that more information is needed to think about an architecture, the most simple way to create different kind of objects that behave the same (i.e. all of them will generate an output) is using the factory pattern. More info here

The factory method pattern is an object-oriented creational design pattern to implement the concept of factories and deals with the problem of creating objects (products) without specifying the exact class of object that will be created. The essence of this pattern is to "Define an interface for creating an object, but let the classes that implement the interface decide which class to instantiate. The Factory method lets a class defer instantiation to subclasses." From wikipedia

3
  • 1
    I think it's a little more involved than that. For example, what protocols will be used to communicate data along the lines in the diagram? Can there be a common data representation in the Rendering/layout engine, or is that engine just a factory for completely custom methods, one for each line in the diagram? Commented Jun 6, 2013 at 4:02
  • Not sure If I get your point here. Because If need to use a protocol to communicate the lines in the diagram then I'm thinking that I'm relying on a set of services to generate the exports (in this case you will like to see some soa/integration patterns). Even if this is true the solution is flexible and robust enough to use the factory. Maybe the thing that you want to do is to create a converter interface that has two methods: one that receives the XML data and another for JSON data. The return object for both will be the converted object. That way you can assemble whatever you want.
    – Orposuser
    Commented Jun 11, 2013 at 4:12
  • there are actually two question in the header: about content (gif, pdf, html) and about transport (local file, http-response-item). To expand @Orposuser (+1) answer: I would create a stream using a factory that can be easily unittested and easyly rendered for http-response.
    – k3b
    Commented Jul 15, 2013 at 14:24
0

You could end up with something like this.

The two factories are based around:

1 - for converting the input type (Json/XML) to a concrete implementation of how to convert this data to an image/graph

2 - A second factory for deciding how render the output to a word Document/PDF Document

The Polymorphism uses a common interface for all rendered data. So an image/table can be moved around as an easy interface.

1 - Factory to convert JSON/XML data to a concrete implementation:

public enum DataTypeToConvertTo
{
    Image,
    Table,
    Graph,
    OtherData
}

public interface IDataConverter
{
    IConvertedData ConvertJsonDataToOutput(Json jsonData);
    IConvertedData ConvertXmlDataToOutput(XDocument xmlData);
}

public abstract class DataConverter : IDataConverter
{
    public DataConverter()
    {

    }

    public abstract IConvertedData ConvertDataToOutput(string data);
}

The Factory below allows you to convert the xml Data or Json Data to the correct concrete type.

public class DataConverterFactory
{
    public static IDataConverter GetDataConverter(DataTypeToConvertTo dataType)
    {
        switch(dataType)
        {
            case DataTypeToConvertTo.Image:
                return new ImageDataConverter();
            case DataTypeToConvertTo.Table:
                return new TableDataConverter();
            case DataTypeToConvertTo.OtherData:
                return new OtherDataConverter();
            default:
                throw new Exception("Unknown DataTypeToConvertTo");
        }
    }
}

The concrete implementations do all the heavy work of converted the data to the relevant type. They also convert the data to the IConvertedData interface, which is used for the polymorphism.

public sealed class ImageDataConverter : DataConverter
{
    public ImageDataConverter()
        : base()
    {

    }

    public override IConvertedData ConvertJsonDataToOutput(Json jsonData)
    {
        var convertedData = new ImageConvertedData();
        //Logic to convert to necessary datatype

        return convertedData;
    }

    public override IConvertedData ConvertXmlDataToOutput(XDocument xmlData)
    {
        var convertedData = new ImageConvertedData();
        //Logic to convert to necessary datatype

        return convertedData;
    }
}

public sealed class TableDataConverter : DataConverter
{
    public TableDataConverter()
        : base()
    {

    }

    public override IConvertedData ConvertJsonDataToOutput(Json jsonData)
    {
        var convertedData = new TableConvertedData();
        //Logic to convert to necessary datatype

        return convertedData;
    }

    public override IConvertedData ConvertXmlDataToOutput(XDocument xmlData)
    {
        var convertedData = new ImageConvertedData();
        //Logic to convert to necessary datatype

        return convertedData;
    }
}

public sealed class OtherDataConverter : DataConverter
{
    public OtherDataConverter()
        : base()
    {

    }

    public override IConvertedData ConvertJsonDataToOutput(Json jsonData)
    {
        var convertedData = new OtherConvertedData();
        //Logic to convert to necessary datatype

        return convertedData;
    }

    public override IConvertedData ConvertXmlDataToOutput(XDocument xmlData)
    {
        var convertedData = new OtherConvertedData();
        //Logic to convert to necessary datatype

        return convertedData;
    }
}

You can add these implementations as necessary, as your code expands.

The IConvertedData interface allows you to pass a single type into the next phase: NOTE: You may not be returning voids here. It could by a byte[] for images or an OpenXml document for the WordDocument. Adjust as necessary.

public interface IConvertedData
{
    void RenderToPdf();
    void RenderToDocument();
    void RenderToOhter();
    void RenderToPowerPoint();
}

Polymorphism:

This is used to convert the data to the relevant output type. i.e. the rendering to PDF for image data, may be different rendering image data for PowerPoint.

public sealed class ImageConvertedData : IConvertedData
{
    public void RenderToPdf()
    {
        //Logic to render Images
    }

    public void RenderToDocument()
    {
        //Logic to render Images
    }
}
public sealed class TableConvertedData : IConvertedData
{
    public void RenderToPdf()
    {
        //Logic to render Document
    }

    public void RenderToDocument()
    {
        //Logic to render Document
    }
}

public sealed class OtherConvertedData : IConvertedData
{
    public void RenderToPdf()
    {
        //Logic to render PDF
    }

    public void RenderToDocument()
    {
        //Logic to render PDF
    }
}

2 - Factory to decide the output format:

public enum ExportOutputType
{
    PDF,
    PowerPoint,
    Word,
    Other
}

public interface IOutputExporter
{
    void ExportData(IConvertedData data);
}


public class OutputExporterFactory
{
    public static IOutputExporter GetExportOutputType(ExportOutputType exportOutputType)
    {
        switch(exportOutputType)
        {
            case ExportOutputType.PDF:
                return new OutputExporterPdf();
            case ExportOutputType.PowerPoint:
                return new OutputExporterPowerPoint();
            case ExportOutputType.Other:
                return new OutputExporterOther();
            default:
                throw new Exception ("Unknown ExportOutputType");
        }
    }
}

Each concrete implementation exposes a common method that masks how the exporting is being thrown back to the IConvertedData implementations

public abstract class OutputExporter : IOutputExporter
{
    //Other base methods...
    public virtual void ExportData(IConvertedData data)
    {

    }
}

public sealed class OutputExporterPdf : OutputExporter
{
    public OutputExporterPdf()
        : base()
    {

    }

    public override void ExportData(IConvertedData data)
    {
        //Functionality to Export to Pdf
        data.RenderToPdf();
    }
}

public sealed class OutputExporterPowerPoint : OutputExporter
{
    public OutputExporterPowerPoint()
        : base()
    {

    }

    public override void ExportData(IConvertedData data)
    {
        //Functionality to Export to PowerPoint
        data.RenderToPowerPoint();
    }
}

public sealed class OutputExporterOther : OutputExporter
{
    public OutputExporterOther()
        : base()
    {

    }

    public override void ExportData(IConvertedData data)
    {
        //Functionality to Export to PowerPoint
        data.RenderToOhter();
    }
}

A sample client for all this would be:

public class Client
{
    public Client()
    {

    }
    public void StartExportProcess(XDocument data)
    {
        IDataConverter converter = DataConverterFactory.GetDataConverter(DataTypeToConvertTo.Graph);

        IConvertedData convertedData = converter.ConvertXmlDataToOutput(data);


        IOutputExporter exportOutputer = OutputExporterFactory.GetExportOutputType(ExportOutputType.PDF);
        exportOutputer.ExportData(convertedData);
    }
}
0

We solved a similar problem here: https://ergebnisse.zensus2011.de/?locale=en There we have mostly "tables" and "graphs" to be exported in different formats: pdf, excel, web. Our idea was to specify each object to be rendered as an own Java class with interfaces to create and read those classes. In your case there would be 2 implementations for each object for creating (xml, json) and 4 implementations for rendering (reading).

Example: You will need some classes for Tables: Class Table (handles table structure, validation and contents) Interface CreateTable(provides table data, cells, spans, content) Interface ReadTable(getters for all data)

Probably you don't need the interfaces (or only one) but i think it always provides a good decoupling especially useful in testing.

0

I think what you're looking for is the Strategy pattern. You have a variety of classes to output the data in the desired format, and you simply choose the appropriate one at runtime. Adding a new format should be as simple as adding another class that implements the required interface. I've done this often in Java using Spring to simply maintain a map of converters, keyed by the format type.

As others have mentioned, this is usually accomplished by having all classes implement the same interface (or descend from the same base class) and choosing the implementation via a factory.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.