Projektujesz solidną architekturę dla wielu typów eksportu?

10

Szukam wzorów lub wskazówek architektonicznych dla nadchodzącej funkcji, którą projektuję. Zasadniczo jest to funkcja eksportu z wieloma celami eksportu i szukam sposobu, aby uczynić ją wystarczająco ogólną, w której podłączanie nowych celów eksportu nie wymaga wielu podstawowych zmian. Podając cele eksportu, po prostu odnoszę się do różnych typów danych wyjściowych, niezależnie od tego, czy są to pliki PDF, prezentacje PowerPoint, dokumenty Word, RSS itp. Mam podstawowy zestaw danych, który jest reprezentowany w JSON i XML. Dane te są wykorzystywane do tworzenia obrazów (przy użyciu dowolnej liczby lub typów eksportu [np. PNG, JPG, GIF itp.), Wykresów, reprezentacji tekstowych, tabel i innych.

Próbuję znaleźć sposób na wyodrębnienie całego renderowania i układu w jakiś silnik renderowania lub układu, który obsługuje dodawanie kolejnych celów eksportowych. Każda pomoc / sugestie / zasoby dotyczące tego, jak podejść do tego, byłyby bardzo mile widziane. Z góry dziękuję.

Dla obrazowego przedstawienia tego, co próbuję osiągnąć.

wprowadź opis zdjęcia tutaj

naiwny programista
źródło
Czy możesz opisać to, czego próbowałeś do tej pory? Jakie są wymagania (obowiązki) silnika układu? Na przykład, czy oczekuje się, że poradzi sobie z paginacją i wyborem rozmiaru strony?
rwong,
Czy danych XML / JSON można używać do tworzenia wielu typów danych wyjściowych w tym samym przebiegu wyjściowym, tj. Dane XML generują obrazy, tabele i wykresy w dokumencie PDF? Czy dane XML / JSON można wykorzystać tylko do utworzenia tabeli lub wykresu dla dokumentu PDF?
Gibson,
Chodzi o xkcd.com/927 - dlaczego próbujesz wynaleźć koło na nowo? DocBook, Markdown / pandoc itp. Już istnieją ...
Deer Hunter

Odpowiedzi:

2

Dla mnie właściwą drogą byłyby interfejsy i fabryka. Jeden, który zwraca odniesienia do interfejsów, za którymi mogą się ukrywać różne klasy. Klasy, które wykonują rzeczywistą pracę z pomrukami, muszą zostać zarejestrowane w fabryce, aby wiedziała, którą klasę utworzyć, biorąc pod uwagę zestaw parametrów.

Uwaga: zamiast interfejsów można również używać abstrakcyjnych klas bazowych, ale wadą jest to, że w przypadku pojedynczych języków dziedziczenia ogranicza cię to do pojedynczej klasy bazowej.

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

Kod ma składnię Delphi (Pascal), ponieważ jest to język, z którym jestem najbardziej zaznajomiony.

Po zarejestrowaniu wszystkich klas implementujących w fabryce powinieneś móc poprosić o odwołanie do interfejsu dla instancji takiej klasy. Na przykład:

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

powinien zwrócić odwołanie IReader do instancji TXMLReader; odwołanie IWriter do wystąpienia TPowerPointWriter i odwołanie IRepresentation do wystąpienia THTMLTable.

Teraz wszystko, co musi zrobić silnik renderowania, to powiązanie wszystkiego razem:

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;

Interfejs IReadera powinien zapewniać metody odczytu danych potrzebnych implementatorom IRepresentation do konstruowania reprezentacji danych. Podobnie IReprezentacja powinna zapewnić metody, których implementatorzy IWriter potrzebują do eksportowania reprezentacji danych do żądanego formatu pliku eksportu.

Zakładając, że dane w twoich plikach mają charakter tabelaryczny, IReader i obsługiwane przez niego interfejsy mogą wyglądać następująco:

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;

Iteracja nad stołem byłaby wtedy kwestią

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;

Ponieważ reprezentacje mogą mieć obrazy, wykresy i charakter tekstowy, IRpresentation prawdopodobnie miałby podobne metody do IReadera do przechodzenia przez skonstruowaną tabelę i miałby metody uzyskiwania obrazów i wykresów, na przykład jako strumień bajtów. Do implementatorów IWriter należałoby zakodowanie wartości tabeli i bajtów obrazu / wykresu zgodnie z wymaganiami celu eksportu.

Marjan Venema
źródło
1

Chociaż zgadzam się, że potrzeba więcej informacji, aby pomyśleć o architekturze, najprostszym sposobem tworzenia różnego rodzaju obiektów, które zachowują się tak samo (tj. Wszystkie wygenerują dane wyjściowe), jest użycie wzorca fabrycznego. Więcej informacji tutaj

Wzorzec metody fabrycznej to obiektowy wzorzec projektowania kreacyjnego służący do implementacji koncepcji fabryk i zajmujący się problemem tworzenia obiektów (produktów) bez określania dokładnej klasy obiektów, które zostaną utworzone. Istotą tego wzorca jest: „Zdefiniować interfejs do tworzenia obiektu, ale pozwolić klasom, które implementują interfejs, zdecydować, która klasa ma zostać utworzona. Metoda Factory umożliwia odłożenie instancji klasy na podklasy”. Z wikipedii

Orposuser
źródło
1
Myślę, że jest to trochę bardziej zaangażowane. Na przykład, jakie protokoły zostaną wykorzystane do przesyłania danych wzdłuż linii na schemacie? Czy może istnieć wspólna reprezentacja danych w silniku renderowania / układzie, czy też silnik ten jest tylko fabryką dla całkowicie niestandardowych metod, po jednej dla każdej linii na schemacie?
Robert Harvey
Nie jestem pewien, czy rozumiem o co ci chodzi. Ponieważ jeśli potrzebuję użyć protokołu do komunikacji linii na schemacie, myślę, że polegam na zestawie usług do generowania eksportu (w tym przypadku chciałbyś zobaczyć niektóre wzorce soa / integracji). Nawet jeśli jest to prawdą, rozwiązanie jest wystarczająco elastyczne i solidne, aby korzystać z fabryki. Być może rzeczą, którą chcesz zrobić, jest stworzenie interfejsu konwertera, który ma dwie metody: jedną, która odbiera dane XML, a drugą dla danych JSON. Obiektem zwrotnym dla obu będzie obiekt przekonwertowany. W ten sposób możesz złożyć wszystko, co chcesz.
Orposuser
w nagłówku znajdują się dwa pytania: o treść (gif, pdf, html) i o transporcie (plik lokalny, element odpowiedzi http). Aby rozwinąć odpowiedź @Orposuser (+1): Utworzyłbym strumień przy użyciu fabryki, która może być łatwo nieprzenikniona i łatwo renderowana dla odpowiedzi HTTP.
k3b
0

Możesz skończyć z czymś takim.

Dwie fabryki opierają się na:

1 - do konwersji typu wejściowego (Json / XML) na konkretną implementację sposobu konwertowania tych danych na obraz / wykres

2 - Druga fabryka do decydowania o sposobie renderowania danych wyjściowych do dokumentu Word / Dokument PDF

Polimorfizm używa wspólnego interfejsu dla wszystkich renderowanych danych. Obraz / tabelę można więc przenosić jako prosty interfejs.

1 - Fabryka do konwersji danych JSON / XML do konkretnej implementacji:

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

Fabryka poniżej pozwala przekonwertować dane XML lub Json Data na właściwy typ betonu.

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

Konkretne wdrożenia wykonują całą ciężką pracę polegającą na przekształceniu danych w odpowiedni typ. Konwertują również dane do interfejsu IConvertedData, który jest wykorzystywany w przypadku polimorfizmu.

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

W razie potrzeby możesz dodać te implementacje w miarę rozszerzania kodu.

Interfejs IConvertedData pozwala przekazać pojedynczy typ do następnej fazy: UWAGA: Być może nie zwracasz tutaj pustek. Może to być bajt [] dla obrazów lub dokument OpenXml dla WordDocument. Dostosuj w razie potrzeby.

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

Wielopostaciowość:

Służy do konwersji danych na odpowiedni typ danych wyjściowych. tzn. renderowanie do pliku PDF dla danych obrazu, może być innym renderingiem danych obrazu dla programu 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 - Fabryka decyduje o formacie wyjściowym:

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

Każda konkretna implementacja udostępnia wspólną metodę, która maskuje sposób, w jaki eksport jest zwracany do implementacji 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();
    }
}

Przykładowy klient do tego wszystkiego to:

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
źródło
0

Podobny problem rozwiązaliśmy tutaj: https://ergebnisse.zensus2011.de/?locale=pl Mamy w większości „tabele” i „wykresy” do eksportu w różnych formatach: pdf, excel, web. Naszym pomysłem było określenie każdego obiektu, który ma być renderowany jako własna klasa Java z interfejsami do tworzenia i czytania tych klas. W twoim przypadku będą 2 implementacje dla każdego obiektu do tworzenia (xml, json) i 4 implementacje do renderowania (czytania).

Przykład: Potrzebne będą pewne klasy dla tabel: Tabela klas (obsługuje strukturę tabeli, sprawdzanie poprawności i zawartość) Interfejs CreateTable (udostępnia dane tabeli, komórki, zakresy, treść) Interfejs ReadTable (pobierające wszystkie dane)

Prawdopodobnie nie potrzebujesz interfejsów (lub tylko jednego), ale myślę, że zawsze zapewnia dobre oddzielenie, szczególnie przydatne w testowaniu.

dermoritz
źródło
0

Myślę, że szukasz wzoru strategii . Masz wiele klas do wyprowadzania danych w pożądanym formacie i po prostu wybierasz odpowiednią w czasie wykonywania. Dodanie nowego formatu powinno być tak proste, jak dodanie innej klasy, która implementuje wymagany interfejs. Często robiłem to w Javie, używając Springa, aby po prostu utrzymywać mapę konwerterów, przypisaną do typu formatu.

Jak wspomnieli inni, zazwyczaj jest to realizowane przez zaimplementowanie przez wszystkie klasy tego samego interfejsu (lub zejście z tej samej klasy bazowej) i wybranie implementacji za pośrednictwem fabryki.

TMN
źródło