¿Diseña una arquitectura robusta para múltiples tipos de exportación?

10

Estoy buscando patrones u orientación arquitectónica para una próxima característica que estoy diseñando. Básicamente, es una función de exportación con múltiples objetivos de exportación, y estoy buscando encontrar una manera de hacerlo lo suficientemente genérico donde enchufar nuevos objetivos de exportación no requiera muchos cambios centrales. Por objetivos de exportación, simplemente me refiero a diferentes tipos de salida, ya sean PDF, presentaciones de PowerPoint, documentos de Word, RSS, etc. Tengo un conjunto de datos base, que está representado en JSON y XML. Estos datos se usan para construir imágenes (usando cualquier número o tipos de exportación [por ejemplo, PNG, JPG, GIF, etc.), gráficos, representaciones textuales, tablas y más.

Estoy tratando de encontrar una manera de abstraer todo el renderizado y el diseño en algún tipo de motor de renderizado o diseño que maneje la adición de más objetivos de exportación. Cualquier ayuda / sugerencia / recursos sobre cómo abordar esto sería muy apreciada. Gracias por adelantado.

Para una representación pictórica de lo que estoy tratando de lograr.

ingrese la descripción de la imagen aquí

ingenuo desarrollador
fuente
¿Puedes describir lo que has probado hasta ahora? ¿Cuáles son los requisitos (responsabilidades) del motor de diseño? Por ejemplo, ¿se espera que maneje la paginación y la selección del tamaño de página?
rwong
¿Se pueden usar los datos XML / JSON para crear múltiples tipos de salida en la misma ejecución de salida, es decir, sus datos XML producen Imágenes y Tablas y Gráficos en un documento PDF? ¿O los datos XML / JSON solo se pueden usar para crear una Tabla o un Gráfico para un documento PDF?
Gibson
Esto es todo acerca de xkcd.com/927 , ¿por qué estás tratando de reinventar la rueda? DocBook, Markdown / pandoc, etc. ya existen ...
Deer Hunter

Respuestas:

2

Para mí, el camino a seguir sería interfaces y una fábrica. Uno que devuelve referencias a interfaces detrás de las cuales se pueden ocultar varias clases. Todas las clases que hacen el trabajo real deben estar registradas en Factory para que sepa qué clase instanciar dado un conjunto de parámetros.

Nota: en lugar de interfaces, también podría usar clases base abstractas, pero el inconveniente es que para los lenguajes de herencia única, lo limita a una sola clase base.

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');

El código está en sintaxis Delphi (Pascal) ya que ese es el lenguaje con el que estoy más familiarizado.

Después de que todas las clases de implementación se registren en la fábrica, debería poder solicitar una referencia de interfaz a una instancia de dicha clase. Por ejemplo:

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

debería devolver una referencia de IReader a una instancia de TXMLReader; una referencia de IWriter a una instancia de TPowerPointWriter y una referencia de representación IR a una instancia de THTMLTable.

Ahora todo lo que necesita hacer el motor de renderizado es unir todo:

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;

La interfaz IReader debe proporcionar métodos para leer los datos que necesitan los implementadores de IRepresentation para construir la representación de los datos. De manera similar, IRepresentation debería proporcionar métodos que los implementadores de IWriter necesitan para exportar la representación de datos al formato de archivo de exportación solicitado.

Asumiendo que los datos en sus archivos son de naturaleza tabular, IReader y sus interfaces de soporte podrían verse así:

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;

Iterar sobre una mesa sería una cuestión de

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;

Como las representaciones pueden ser imágenes, gráficos y de naturaleza textual, la representación IR probablemente tendría métodos similares a IReader para atravesar una tabla construida y tendría métodos para obtener las imágenes y gráficos, por ejemplo, como una secuencia de bytes. Correspondería a los implementadores de IWriter codificar los valores de la tabla y los bytes de imagen / gráfico según lo requiera el objetivo de exportación.

Marjan Venema
fuente
1

Si bien estoy de acuerdo en que se necesita más información para pensar en una arquitectura, la forma más simple de crear diferentes tipos de objetos que se comporten de la misma manera (es decir, todos generarán una salida) es usar el patrón de fábrica. Más información aquí.

El patrón de método de fábrica es un patrón de diseño de creación orientado a objetos para implementar el concepto de fábricas y se ocupa del problema de crear objetos (productos) sin especificar la clase exacta de objeto que se creará. La esencia de este patrón es "Definir una interfaz para crear un objeto, pero dejar que las clases que implementan la interfaz decidan qué clase instanciar. El método Factory permite que una clase difiera la instanciación a subclases". De wikipedia

Orposuser
fuente
1
Creo que es un poco más complicado que eso. Por ejemplo, ¿qué protocolos se utilizarán para comunicar datos a lo largo de las líneas del diagrama? ¿Puede haber una representación de datos común en el motor de Renderizado / diseño, o ese motor es solo una fábrica para métodos completamente personalizados, uno para cada línea en el diagrama?
Robert Harvey
No estoy seguro si entiendo tu punto aquí. Porque si necesito usar un protocolo para comunicar las líneas en el diagrama, entonces estoy pensando que estoy confiando en un conjunto de servicios para generar las exportaciones (en este caso, le gustaría ver algunos patrones de integración / soa). Incluso si esto es cierto, la solución es lo suficientemente flexible y robusta como para usar la fábrica. Quizás lo que quiera hacer es crear una interfaz de convertidor que tenga dos métodos: uno que reciba los datos XML y otro para los datos JSON. El objeto de retorno para ambos será el objeto convertido. De esa manera puedes armar lo que quieras.
Orposuser
En realidad, hay dos preguntas en el encabezado: sobre el contenido (gif, pdf, html) y sobre el transporte (archivo local, elemento de respuesta http). Para expandir la respuesta de @Orposuser (+1): crearía una secuencia usando una fábrica que pueda ser fácilmente probada y procesada fácilmente para la respuesta http.
k3b
0

Podrías terminar con algo como esto.

Las dos fábricas se basan en:

1 - para convertir el tipo de entrada (Json / XML) en una implementación concreta de cómo convertir estos datos en una imagen / gráfico

2 - Una segunda fábrica para decidir cómo renderizar la salida a un documento de Word / documento PDF

El polimorfismo utiliza una interfaz común para todos los datos renderizados. Por lo tanto, una imagen / tabla se puede mover como una interfaz fácil.

1 - Fábrica para convertir datos JSON / XML en una implementación concreta:

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

La Fábrica a continuación le permite convertir los Datos xml o los Datos Json al tipo concreto correcto.

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

Las implementaciones concretas hacen todo el trabajo pesado de convertir los datos al tipo relevante. También convierten los datos a la interfaz IConvertedData, que se utiliza para el polimorfismo.

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

Puede agregar estas implementaciones según sea necesario, a medida que su código se expande.

La interfaz IConvertedData le permite pasar un solo tipo a la siguiente fase: NOTA: es posible que no devuelva vacíos aquí. Podría ser por un byte [] para imágenes o un documento OpenXml para WordDocument. Ajuste según sea necesario.

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

Polimorfismo:

Esto se utiliza para convertir los datos al tipo de salida relevante. es decir, el renderizado a PDF para datos de imagen, puede ser diferente al renderizar datos de imagen para 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 - Fábrica para decidir el formato de salida:

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

Cada implementación concreta expone un método común que enmascara cómo la exportación se devuelve a las implementaciones de IConvertedData

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

Un cliente de muestra para todo esto sería:

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);
    }
}
Gibson
fuente
0

Resolvimos un problema similar aquí: https://ergebnisse.zensus2011.de/?locale=en Allí tenemos principalmente "tablas" y "gráficos" para exportar en diferentes formatos: pdf, excel, web. Nuestra idea era especificar que cada objeto se representara como una clase Java propia con interfaces para crear y leer esas clases. En su caso, habría 2 implementaciones para cada objeto para crear (xml, json) y 4 implementaciones para renderizar (leer).

Ejemplo: Necesitará algunas clases para las Tablas: Tabla de clase (maneja la estructura de la tabla, validación y contenido) Interfaz CreateTable (proporciona datos de tabla, celdas, espacios, contenido) Interfaz ReadTable (captadores para todos los datos)

Probablemente no necesite las interfaces (o solo una), pero creo que siempre proporciona un buen desacoplamiento especialmente útil en las pruebas.

dermoritz
fuente
0

Creo que lo que estás buscando es el patrón de Estrategia . Tiene una variedad de clases para generar los datos en el formato deseado, y simplemente elige la apropiada en tiempo de ejecución. Agregar un nuevo formato debería ser tan simple como agregar otra clase que implemente la interfaz requerida. He hecho esto a menudo en Java usando Spring para simplemente mantener un mapa de convertidores, clave por tipo de formato.

Como otros han mencionado, esto generalmente se logra haciendo que todas las clases implementen la misma interfaz (o desciendan de la misma clase base) y eligiendo la implementación a través de una fábrica.

TMN
fuente