Sto scrivendo un'applicazione C ++. La maggior parte delle applicazioni legge e scrive citazioni di dati necessarie e questa non fa eccezione. Ho creato un design di alto livello per il modello di dati e la logica di serializzazione. Questa domanda richiede una revisione del mio progetto tenendo presenti questi obiettivi specifici:
Per avere un modo semplice e flessibile di leggere e scrivere modelli di dati in formati arbitrari: binario grezzo, XML, JSON, et. al. Il formato dei dati dovrebbe essere disaccoppiato dai dati stessi e dal codice che richiede la serializzazione.
Garantire che la serializzazione sia il più possibile priva di errori. L'I / O è intrinsecamente rischioso per una serie di motivi: il mio progetto introduce più modi per fallire? In tal caso, come potrei riformattare il progetto per mitigare tali rischi?
Questo progetto utilizza C ++. Che tu lo ami o lo odi, il linguaggio ha il suo modo di fare le cose e il design mira a lavorare con il linguaggio, non contro di esso .
Infine, il progetto è basato su wxWidgets . Mentre sto cercando una soluzione applicabile a un caso più generale, questa specifica implementazione dovrebbe funzionare bene con quel toolkit.
Quello che segue è un insieme molto semplice di classi scritte in C ++ che illustrano il design. Queste non sono le classi reali che ho parzialmente scritto finora, questo codice illustra semplicemente il design che sto usando.
Innanzitutto, alcuni DAO di esempio:
#include <iostream>
#include <map>
#include <memory>
#include <string>
#include <vector>
// One widget represents one record in the application.
class Widget {
public:
using id_type = int;
private:
id_type id;
};
// Container for widgets. Much more than a dumb container,
// it will also have indexes and other metadata. This represents
// one data file the user may open in the application.
class WidgetDatabase {
::std::map<Widget::id_type, ::std::shared_ptr<Widget>> widgets;
};
Successivamente, definisco le classi virtuali (interfacce) pure per la lettura e la scrittura di DAO. L'idea è quella di astrarre la serializzazione dei dati dai dati stessi ( SRP ).
class WidgetReader {
public:
virtual Widget read(::std::istream &in) const abstract;
};
class WidgetWriter {
public:
virtual void write(::std::ostream &out, const Widget &widget) const abstract;
};
class WidgetDatabaseReader {
public:
virtual WidgetDatabase read(::std::istream &in) const abstract;
};
class WidgetDatabaseWriter {
public:
virtual void write(::std::ostream &out, const WidgetDatabase &widgetDb) const abstract;
};
Infine, ecco il codice che ottiene il lettore / scrittore appropriato per il tipo di I / O desiderato. Ci sarebbero anche sottoclassi di lettori / scrittori definiti, ma questi non aggiungono nulla alla revisione del design:
enum class WidgetIoType {
BINARY,
JSON,
XML
// Other types TBD.
};
WidgetIoType forFilename(::std::string &name) { return ...; }
class WidgetIoFactory {
public:
static ::std::unique_ptr<WidgetReader> getWidgetReader(const WidgetIoType &type) {
return ::std::unique_ptr<WidgetReader>(/* TODO */);
}
static ::std::unique_ptr<WidgetWriter> getWidgetWriter(const WidgetIoType &type) {
return ::std::unique_ptr<WidgetWriter>(/* TODO */);
}
static ::std::unique_ptr<WidgetDatabaseReader> getWidgetDatabaseReader(const WidgetIoType &type) {
return ::std::unique_ptr<WidgetDatabaseReader>(/* TODO */);
}
static ::std::unique_ptr<WidgetDatabaseWriter> getWidgetDatabaseWriter(const WidgetIoType &type) {
return ::std::unique_ptr<WidgetDatabaseWriter>(/* TODO */);
}
};
Per gli obiettivi dichiarati del mio progetto, ho una preoccupazione specifica. I flussi C ++ possono essere aperti in modalità testo o binaria, ma non è possibile controllare un flusso già aperto. Potrebbe essere possibile tramite un errore del programmatore fornire ad esempio un flusso binario a un lettore / scrittore XML o JSON. Ciò potrebbe causare errori sottili (o non così sottili). Preferirei che il codice fallisse rapidamente, ma non sono sicuro che questo progetto lo farebbe.
Un modo per aggirare questo problema potrebbe essere quello di scaricare la responsabilità di aprire lo stream al lettore o allo scrittore, ma credo che violi SRP e renderebbe il codice più complesso. Quando si scrive un DAO, lo scrittore non dovrebbe preoccuparsi di dove sta andando lo stream: potrebbe essere un file, standard out, una risposta HTTP, un socket, qualsiasi cosa. Una volta che questa preoccupazione è incapsulata nella logica della serializzazione, diventa molto più complessa: deve conoscere il tipo specifico di flusso e quale costruttore chiamare.
A parte questa opzione, non sono sicuro di quale sarebbe un modo migliore per modellare questi oggetti che è semplice, flessibile e aiuta a prevenire errori logici nel codice che lo utilizza.
Il caso d'uso con cui la soluzione deve essere integrata è una semplice finestra di dialogo per la selezione dei file . L'utente seleziona "Apri ..." o "Salva con nome ..." dal menu File e il programma apre o salva WidgetDatabase. Ci saranno anche le opzioni "Importa ..." e "Esporta ..." per i singoli Widget.
Quando l'utente seleziona un file da aprire o salvare, wxWidgets restituirà un nome file. Il gestore che risponde a quell'evento deve essere un codice generico che accetta il nome del file, acquisisce un serializzatore e chiama una funzione per eseguire il sollevamento di carichi pesanti. Idealmente, questo progetto funzionerebbe anche se un altro pezzo di codice eseguisse operazioni di I / O senza file, come l'invio di un WidgetDatabase a un dispositivo mobile tramite un socket.
Un widget viene salvato nel suo formato? Interagisce con i formati esistenti? Sì! Tutti i precedenti. Tornando alla finestra di dialogo del file, pensa a Microsoft Word. Microsoft era libera di sviluppare il formato DOCX, tuttavia desiderava entro determinati limiti. Allo stesso tempo, Word legge o scrive anche formati legacy e di terze parti (ad es. PDF). Questo programma non è diverso: il formato "binario" di cui parlo è un formato interno ancora da definire progettato per la velocità. Allo stesso tempo, deve essere in grado di leggere e scrivere formati standard aperti nel suo dominio (irrilevante per la domanda) in modo da poter lavorare con altri software.
Infine, esiste un solo tipo di widget. Avrà oggetti figlio, ma questi saranno gestiti da questa logica di serializzazione. Il programma non caricherà mai sia Widget che Pignoni. Questo design unico ha bisogno di essere interessati con i widget e WidgetDatabases.