Concevoir une architecture robuste pour plusieurs types d'export?

10

Je recherche des modèles ou des conseils architecturaux pour une fonctionnalité à venir que je conçois. Fondamentalement, c'est une fonction d'exportation avec plusieurs cibles d'exportation, et je cherche à trouver un moyen de la rendre suffisamment générique où le branchement de nouvelles cibles d'exportation ne nécessite pas beaucoup de changements de base. Par cibles d'exportation, je fais simplement référence à différents types de sortie, qu'il s'agisse de PDF, de présentations PowerPoint, de documents Word, de RSS, etc. J'ai un ensemble de données de base, qui est représenté en JSON et XML. Ces données sont utilisées pour construire des images (en utilisant n'importe quel nombre ou type d'exportation [par exemple, PNG, JPG, GIF, etc.), des graphiques, des représentations textuelles, des tableaux, etc.

J'essaie de trouver un moyen d'abstraire tout le rendu et la mise en page dans une sorte de moteur de rendu ou de mise en page qui gère l'ajout d'autres cibles d'exportation. Toute aide / suggestion / ressource sur la manière d'aborder cela serait grandement appréciée. Merci d'avance.

Pour une représentation imagée de ce que j'essaie de réaliser.

entrez la description de l'image ici

naivedeveloper
la source
Pouvez-vous décrire ce que vous avez essayé jusqu'à présent? Quelles sont les exigences (responsabilités) du moteur de mise en page? Par exemple, est-il censé gérer la pagination et la sélection de la taille de page?
rwong
Les données XML / JSON peuvent-elles être utilisées pour créer plusieurs types de sortie sur la même sortie, c'est-à-dire que vos données XML produisent des images, des tableaux et des graphiques dans un document PDF? Ou les données XML / JSON peuvent-elles uniquement être utilisées pour créer un tableau ou un graphique pour un document PDF?
Gibson
Il s'agit de xkcd.com/927 - pourquoi essayez-vous de réinventer la roue? DocBook, Markdown / pandoc etc. existent déjà ...
Deer Hunter

Réponses:

2

Pour moi, la voie à suivre serait des interfaces et une usine. Celui qui renvoie des références aux interfaces derrière lesquelles diverses classes peuvent se cacher. Les classes qui effectuent le travail de grognement réel doivent toutes être enregistrées auprès de la fabrique afin qu'elle sache quelle classe instancier en fonction d'un ensemble de paramètres.

Remarque: au lieu d'interfaces, vous pouvez également utiliser des classes de base abstraites, mais l'inconvénient est que pour les langages à héritage unique, il vous limite à une seule classe de 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');

Le code est en syntaxe Delphi (Pascal) car c'est le langage que je connais le mieux.

Une fois que toutes les classes d'implémentation sont enregistrées auprès de la fabrique, vous devriez pouvoir demander une référence d'interface à une instance d'une telle classe. Par exemple:

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

doit renvoyer une référence IReader à une instance de TXMLReader; une référence IWriter à une instance de TPowerPointWriter et une référence IRepresentation à une instance de THTMLTable.

Maintenant, tout ce que le moteur de rendu doit faire, c'est tout lier:

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;

L'interface IReader devrait fournir des méthodes pour lire les données nécessaires aux implémenteurs IRepresentation pour construire la représentation des données. De même, IRepresentation doit fournir les méthodes dont les implémenteurs IWriter ont besoin pour exporter la représentation des données au format de fichier d'exportation demandé.

En supposant que les données de vos fichiers sont de nature tabulaire, IReader et ses interfaces de support pourraient ressembler à:

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;

Itérer sur une table serait alors une question 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;

Comme les représentations peuvent être des images, des graphiques et des textes, les représentations IR auraient probablement des méthodes similaires à IReader pour parcourir une table construite et des méthodes pour obtenir les images et les graphiques, par exemple sous la forme d'un flux d'octets. Il appartiendrait aux implémenteurs IWriter d'encoder les valeurs de la table et les octets image / graphique requis par la cible d'exportation.

Marjan Venema
la source
1

Bien que je convienne que plus d'informations sont nécessaires pour penser à une architecture, le moyen le plus simple de créer différents types d'objets qui se comportent de la même manière (c'est-à-dire qu'ils généreront tous une sortie) est d'utiliser le modèle d'usine. Plus d'infos ici

Le modèle de méthode d'usine est un modèle de conception créative orienté objet pour implémenter le concept d'usines et traite le problème de la création d'objets (produits) sans spécifier la classe exacte d'objet qui sera créée. L'essence de ce modèle est de "Définir une interface pour créer un objet, mais laisser les classes qui implémentent l'interface décider quelle classe instancier. La méthode Factory permet à une classe de reporter l'instanciation aux sous-classes." De wikipedia

Orposuser
la source
1
Je pense que c'est un peu plus compliqué que ça. Par exemple, quels protocoles seront utilisés pour communiquer des données le long des lignes du diagramme? Peut-il y avoir une représentation de données commune dans le moteur de rendu / mise en page, ou ce moteur est-il juste une usine pour des méthodes complètement personnalisées, une pour chaque ligne du diagramme?
Robert Harvey
Je ne sais pas si je comprends votre point ici. Parce que si vous avez besoin d'utiliser un protocole pour communiquer les lignes dans le diagramme, je pense que je compte sur un ensemble de services pour générer les exportations (dans ce cas, vous aimerez voir des modèles soa / d'intégration). Même si cela est vrai, la solution est suffisamment flexible et robuste pour utiliser l'usine. Peut-être que la chose que vous voulez faire est de créer une interface de convertisseur qui a deux méthodes: une qui reçoit les données XML et une autre pour les données JSON. L'objet de retour pour les deux sera l'objet converti. De cette façon, vous pouvez assembler ce que vous voulez.
Orposuser
il y a en fait deux questions dans l'en-tête: sur le contenu (gif, pdf, html) et sur le transport (fichier local, http-response-item). Pour développer la réponse @Orposuser (+1): je créerais un flux à l'aide d'une fabrique qui peut être facilement non vérifiée et rendue facilement pour la réponse http.
k3b
0

Vous pourriez vous retrouver avec quelque chose comme ça.

Les deux usines s'articulent autour de:

1 - pour convertir le type d'entrée (Json / XML) en une implémentation concrète de la façon de convertir ces données en image / graphique

2 - Une deuxième usine pour décider comment rendre la sortie en un document Word / Document PDF

Le polymorphisme utilise une interface commune pour toutes les données rendues. Ainsi, une image / table peut être déplacée comme une interface facile.

1 - Factory pour convertir les données JSON / XML en une implémentation concrète:

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

L'usine ci-dessous vous permet de convertir les données xml ou les données Json en type de béton correct.

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

Les implémentations concrètes font tout le travail lourd de conversion des données dans le type approprié. Ils convertissent également les données vers l'interface IConvertedData, qui est utilisée pour le polymorphisme.

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

Vous pouvez ajouter ces implémentations si nécessaire, à mesure que votre code se développe.

L'interface IConvertedData vous permet de passer un seul type dans la phase suivante: REMARQUE: vous ne pouvez pas renvoyer de vides ici. Il peut s'agir d'un octet [] pour les images ou d'un document OpenXml pour le WordDocument. Ajustez si nécessaire.

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

Polymorphisme:

Ceci est utilisé pour convertir les données en type de sortie approprié. c'est-à-dire que le rendu au format PDF des données d'image peut être différent des données d'image de rendu pour 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 - Usine pour décider du format de sortie:

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

Chaque implémentation concrète expose une méthode courante qui masque la façon dont l'exportation est renvoyée aux implémentations 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 exemple de client pour tout cela serait:

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
la source
0

Nous avons résolu un problème similaire ici: https://ergebnisse.zensus2011.de/?locale=en Là, nous avons principalement des "tableaux" et des "graphiques" à exporter dans différents formats: pdf, excel, web. Notre idée était de spécifier chaque objet à rendre comme une propre classe Java avec des interfaces pour créer et lire ces classes. Dans votre cas, il y aurait 2 implémentations pour chaque objet pour la création (xml, json) et 4 implémentations pour le rendu (lecture).

Exemple: Vous aurez besoin de certaines classes pour les tables: Table de classe (gère la structure de la table, la validation et le contenu) Interface CreateTable (fournit les données du tableau, les cellules, les étendues, le contenu) Interface ReadTable (getters pour toutes les données)

Vous n'avez probablement pas besoin des interfaces (ou d'une seule) mais je pense que cela fournit toujours un bon découplage particulièrement utile pour les tests.

dermoritz
la source
0

Je pense que ce que vous cherchez, c'est le modèle de stratégie . Vous disposez d'une variété de classes pour produire les données dans le format souhaité, et vous choisissez simplement celle qui convient lors de l'exécution. L'ajout d'un nouveau format doit être aussi simple que l'ajout d'une autre classe qui implémente l'interface requise. Je l'ai fait souvent en Java en utilisant Spring pour simplement maintenir une carte de convertisseurs, indexée par le type de format.

Comme d'autres l'ont mentionné, cela se fait généralement en faisant en sorte que toutes les classes implémentent la même interface (ou descendent de la même classe de base) et en choisissant l'implémentation via une usine.

TMN
la source