Entwerfen einer robusten Architektur für mehrere Exporttypen?

10

Ich suche nach Mustern oder architektonischen Anleitungen für ein bevorstehendes Feature, das ich entwerfe. Grundsätzlich handelt es sich um eine Exportfunktion mit mehreren Exportzielen, und ich suche nach einer Möglichkeit, sie generisch genug zu gestalten, wenn das Einfügen neuer Exportziele nicht viele grundlegende Änderungen erfordert. Bei Exportzielen beziehe ich mich einfach auf verschiedene Arten von Ausgaben, sei es PDFs, PowerPoint-Präsentationen, Word-Dokumente, RSS usw. Ich habe einen Basisdatensatz, der in JSON und XML dargestellt wird. Diese Daten werden verwendet, um Bilder (unter Verwendung einer beliebigen Anzahl oder Exporttypen [z. B. PNG, JPG, GIF usw.), Grafiken, Textdarstellungen, Tabellen und mehr zu erstellen.

Ich versuche einen Weg zu finden, um das gesamte Rendering und Layout in eine Art Rendering- oder Layout-Engine zu abstrahieren, die das Hinzufügen weiterer Exportziele übernimmt. Jede Hilfe / Anregungen / Ressourcen, wie man dies angeht, wäre sehr dankbar. Danke im Voraus.

Für eine bildliche Darstellung dessen, was ich erreichen möchte.

Geben Sie hier die Bildbeschreibung ein

naiver Entwickler
quelle
Können Sie beschreiben, was Sie bisher versucht haben? Was sind die Anforderungen (Verantwortlichkeiten) der Layout-Engine? Wird beispielsweise erwartet, dass die Paginierung und die Auswahl der Seitengröße behandelt werden?
Rwong
Können die XML / JSON-Daten verwendet werden, um mehrere Ausgabetypen im selben Ausgabelauf zu erstellen, dh Ihre XML-Daten erzeugen Bilder, Tabellen und Grafiken in einem PDF-Dokument? Oder können die XML / JSON-Daten nur zum Erstellen einer Tabelle oder eines Diagramms für ein PDF-Dokument verwendet werden?
Gibson
Hier dreht sich alles um xkcd.com/927 - warum versuchen Sie, das Rad neu zu erfinden? DocBook, Markdown / Pandoc usw. existieren bereits ...
Deer Hunter

Antworten:

2

Für mich wären Schnittstellen und eine Fabrik der richtige Weg. Eine, die Verweise auf Schnittstellen zurückgibt, hinter denen sich verschiedene Klassen verstecken können. Die Klassen, die das eigentliche Grunzen ausführen, müssen alle bei der Factory registriert sein, damit sie wissen, welche Klasse bei einer Reihe von Parametern instanziiert werden muss.

Hinweis: Anstelle von Schnittstellen können Sie auch abstrakte Basisklassen verwenden. Der Nachteil besteht jedoch darin, dass Sie für einzelne Vererbungssprachen auf eine einzelne Basisklasse beschränkt sind.

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 ist in Delphi (Pascal) -Syntax, da dies die Sprache ist, mit der ich am besten vertraut bin.

Nachdem alle implementierenden Klassen bei der Factory registriert wurden, sollten Sie in der Lage sein, einen Schnittstellenverweis auf eine Instanz einer solchen Klasse anzufordern. Zum Beispiel:

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

sollte einen IReader-Verweis auf eine Instanz von TXMLReader zurückgeben; eine IWriter-Referenz auf eine Instanz von TPowerPointWriter und eine IRepresentation-Referenz auf eine Instanz von THTMLTable.

Jetzt muss die Rendering-Engine nur noch alles zusammenbinden:

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;

Die IReader-Schnittstelle sollte Methoden zum Lesen der Daten bereitstellen, die von IRepresentation-Implementierern zum Erstellen der Darstellung der Daten benötigt werden. Ebenso sollte IRepresentation Methoden bereitstellen, die IWriter-Implementierer benötigen, um die Datendarstellung in das angeforderte Exportdateiformat zu exportieren.

Unter der Annahme, dass die Daten in Ihren Dateien tabellarischer Natur sind, könnten IReader und seine unterstützenden Schnittstellen folgendermaßen aussehen:

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;

Über einen Tisch zu iterieren wäre dann eine Frage von

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;

Da es sich bei den Darstellungen um Bilder, Grafiken und Text handelt, verfügt die IR-Darstellung wahrscheinlich über ähnliche Methoden wie IReader zum Durchlaufen einer konstruierten Tabelle und über Methoden zum Abrufen der Bilder und Grafiken, beispielsweise als Bytestrom. Es wäre Sache der IWriter-Implementierer, die Tabellenwerte und die Bild- / Grafikbytes gemäß den Anforderungen des Exportziels zu codieren.

Marjan Venema
quelle
1

Ich stimme zu, dass mehr Informationen erforderlich sind, um über eine Architektur nachzudenken. Der einfachste Weg, verschiedene Arten von Objekten zu erstellen, die sich gleich verhalten (dh alle erzeugen eine Ausgabe), ist die Verwendung des Factory-Musters. Mehr Infos hier

Das Factory-Methodenmuster ist ein objektorientiertes kreatives Entwurfsmuster zur Implementierung des Fabrikkonzepts und befasst sich mit dem Problem der Erstellung von Objekten (Produkten), ohne die genaue Klasse des zu erstellenden Objekts anzugeben. Die Essenz dieses Musters besteht darin, "eine Schnittstelle zum Erstellen eines Objekts zu definieren, aber die Klassen, die die Schnittstelle implementieren, entscheiden zu lassen, welche Klasse instanziiert werden soll. Mit der Factory-Methode kann eine Klasse die Instanziierung auf Unterklassen verschieben." Aus Wikipedia

Orposuser
quelle
1
Ich denke, es ist etwas komplizierter. Welche Protokolle werden beispielsweise verwendet, um Daten entlang der Linien im Diagramm zu kommunizieren? Kann es eine gemeinsame Datendarstellung in der Rendering- / Layout-Engine geben, oder ist diese Engine nur eine Factory für vollständig benutzerdefinierte Methoden, eine für jede Zeile im Diagramm?
Robert Harvey
Ich bin mir nicht sicher, ob ich Ihren Standpunkt hier verstehe. Denn wenn Sie ein Protokoll verwenden müssen, um die Linien im Diagramm zu kommunizieren, denke ich, dass ich mich auf eine Reihe von Diensten verlasse, um die Exporte zu generieren (in diesem Fall möchten Sie einige Soa- / Integrationsmuster sehen). Selbst wenn dies zutrifft, ist die Lösung flexibel und robust genug, um ab Werk verwendet zu werden. Möglicherweise möchten Sie eine Konverterschnittstelle mit zwei Methoden erstellen: eine, die die XML-Daten empfängt, und eine andere für JSON-Daten. Das Rückgabeobjekt für beide ist das konvertierte Objekt. Auf diese Weise können Sie alles zusammenbauen, was Sie wollen.
Orposuser
Der Header enthält zwei Fragen: zum Inhalt (gif, pdf, html) und zum Transport (lokale Datei, http-response-item). So erweitern Sie die Antwort von @Orposuser (+1): Ich würde einen Stream mit einer Factory erstellen, die leicht ungetestet und für die http-Antwort leicht gerendert werden kann.
k3b
0

Sie könnten mit so etwas enden.

Die beiden Fabriken basieren auf:

1 - zum Konvertieren des Eingabetyps (Json / XML) in eine konkrete Implementierung zum Konvertieren dieser Daten in ein Bild / Diagramm

2 - Eine zweite Factory zur Entscheidung, wie die Ausgabe in ein Word-Dokument / PDF-Dokument gerendert werden soll

Der Polymorphismus verwendet eine gemeinsame Schnittstelle für alle gerenderten Daten. So kann ein Bild / eine Tabelle als einfache Oberfläche verschoben werden.

1 - Factory zum Konvertieren von JSON / XML-Daten in eine konkrete Implementierung:

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

In der folgenden Factory können Sie die XML-Daten oder Json-Daten in den richtigen konkreten Typ konvertieren.

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

Die konkreten Implementierungen erledigen die ganze schwere Arbeit der Konvertierung der Daten in den entsprechenden Typ. Sie konvertieren die Daten auch in die IConvertedData-Schnittstelle, die für den Polymorphismus verwendet wird.

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

Sie können diese Implementierungen nach Bedarf hinzufügen, wenn Ihr Code erweitert wird.

Über die IConvertedData-Schnittstelle können Sie einen einzelnen Typ in die nächste Phase übergeben: HINWEIS: Möglicherweise geben Sie hier keine Hohlräume zurück. Dies kann ein Byte [] für Bilder oder ein OpenXml-Dokument für das WordDocument sein. Bei Bedarf anpassen.

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

Polymorphismus:

Dies wird verwendet, um die Daten in den entsprechenden Ausgabetyp zu konvertieren. dh das Rendern in PDF für Bilddaten kann unterschiedliche Rendering-Bilddaten für PowerPoint sein.

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 - Werkseitig das Ausgabeformat festlegen:

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

Jede konkrete Implementierung stellt eine allgemeine Methode bereit, die maskiert, wie der Export in die IConvertedData-Implementierungen zurückgeworfen wird

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

Ein Beispiel-Client für all dies wäre:

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
quelle
0

Wir haben hier ein ähnliches Problem gelöst: https://ergebnisse.zensus2011.de/?locale=de Dort haben wir meistens "Tabellen" und "Grafiken", die in verschiedenen Formaten exportiert werden sollen: pdf, excel, web. Unsere Idee war es, jedes Objekt, das gerendert werden soll, als eigene Java-Klasse mit Schnittstellen zum Erstellen und Lesen dieser Klassen anzugeben. In Ihrem Fall gibt es 2 Implementierungen für jedes Objekt zum Erstellen (xml, json) und 4 Implementierungen zum Rendern (Lesen).

Beispiel: Sie benötigen einige Klassen für Tabellen: Klassentabelle (behandelt Tabellenstruktur, Validierung und Inhalt) Schnittstelle CreateTable (enthält Tabellendaten, Zellen, Bereiche, Inhalte) Schnittstelle ReadTable (Getter für alle Daten)

Wahrscheinlich brauchen Sie nicht die Schnittstellen (oder nur eine), aber ich denke, es bietet immer eine gute Entkopplung, die besonders beim Testen nützlich ist.

dermoritz
quelle
0

Ich denke, was Sie suchen, ist das Strategiemuster . Sie haben verschiedene Klassen, um die Daten im gewünschten Format auszugeben, und wählen zur Laufzeit einfach die entsprechende aus. Das Hinzufügen eines neuen Formats sollte so einfach sein wie das Hinzufügen einer weiteren Klasse, die die erforderliche Schnittstelle implementiert. Ich habe dies in Java häufig mit Spring gemacht, um einfach eine Karte von Konvertern zu verwalten, die nach dem Formattyp sortiert ist.

Wie andere bereits erwähnt haben, wird dies normalerweise dadurch erreicht, dass alle Klassen dieselbe Schnittstelle implementieren (oder von derselben Basisklasse abstammen) und die Implementierung über eine Factory auswählen.

TMN
quelle