J'écris une application C ++. La plupart des applications lisent et écrivent la citation de données nécessaire et celle-ci ne fait pas exception. J'ai créé une conception de haut niveau pour le modèle de données et la logique de sérialisation. Cette question demande une révision de ma conception avec ces objectifs spécifiques à l'esprit:
Avoir un moyen simple et flexible de lire et d'écrire des modèles de données dans des formats arbitraires: binaire brut, XML, JSON, et. Al. Le format des données doit être découplé des données elles-mêmes ainsi que du code qui demande la sérialisation.
Pour garantir que la sérialisation est aussi exempte d'erreurs que raisonnablement possible. Les E / S sont intrinsèquement risquées pour diverses raisons: ma conception présente-t-elle davantage de façons d'échouer? Si oui, comment pourrais-je refactoriser la conception afin d'atténuer ces risques?
Ce projet utilise C ++. Que vous l'aimiez ou que vous le détestiez, la langue a sa propre façon de faire les choses et la conception vise à travailler avec la langue, pas contre elle .
Enfin, le projet est construit sur wxWidgets . Bien que je recherche une solution applicable à un cas plus général, cette implémentation spécifique devrait bien fonctionner avec cette boîte à outils.
Ce qui suit est un ensemble très simple de classes écrites en C ++ qui illustrent la conception. Ce ne sont pas les classes réelles que j'ai partiellement écrites jusqu'à présent, ce code illustre simplement la conception que j'utilise.
Tout d'abord, quelques exemples de DAO:
#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;
};
Ensuite, je définis des classes virtuelles pures (interfaces) pour lire et écrire des DAO. L'idée est d'abstraire la sérialisation des données des données elles-mêmes ( 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;
};
Enfin, voici le code qui obtient le lecteur / enregistreur approprié pour le type d'E / S souhaité. Il y aurait des sous-classes de lecteurs / auteurs également définies, mais celles-ci n'ajoutent rien à la revue de conception:
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 */);
}
};
Selon les objectifs énoncés de ma conception, j'ai une préoccupation particulière. Les flux C ++ peuvent être ouverts en mode texte ou binaire, mais il n'y a aucun moyen de vérifier un flux déjà ouvert. Il pourrait être possible, grâce à une erreur de programmation, de fournir, par exemple, un flux binaire à un lecteur / graveur XML ou JSON. Cela pourrait provoquer des erreurs subtiles (ou pas si subtiles). Je préférerais que le code échoue rapidement, mais je ne suis pas sûr que cette conception le ferait.
Un moyen de contourner cela pourrait être de décharger la responsabilité d'ouvrir le flux au lecteur ou à l'écrivain, mais je crois que cela viole SRP et rendrait le code plus complexe. Lors de l'écriture d'un DAO, l'auteur ne doit pas se soucier de la destination du flux: il peut s'agir d'un fichier, d'une sortie standard, d'une réponse HTTP, d'une socket, de n'importe quoi. Une fois que cette préoccupation est encapsulée dans la logique de sérialisation, elle devient beaucoup plus complexe: elle doit connaître le type spécifique de flux et le constructeur à appeler.
Mis à part cette option, je ne sais pas quelle serait une meilleure façon de modéliser ces objets qui est simple, flexible et aide à prévenir les erreurs logiques dans le code qui l'utilise.
Le cas d'utilisation avec lequel la solution doit être intégrée est une simple boîte de dialogue de sélection de fichiers . L'utilisateur sélectionne "Ouvrir ..." ou "Enregistrer sous ..." dans le menu Fichier, et le programme ouvre ou enregistre la WidgetDatabase. Il y aura également des options "Importer ..." et "Exporter ..." pour les widgets individuels.
Lorsque l'utilisateur sélectionne un fichier à ouvrir ou à enregistrer, wxWidgets renverra un nom de fichier. Le gestionnaire qui répond à cet événement doit être un code à usage général qui prend le nom de fichier, acquiert un sérialiseur et appelle une fonction pour faire le gros du travail. Idéalement, cette conception fonctionnerait également si un autre morceau de code effectue des E / S non-fichiers, comme l'envoi d'une WidgetDatabase à un appareil mobile via un socket.
Un widget enregistre-t-il dans son propre format? Interagit-il avec les formats existants? Oui! Tout ce qui précède. Pour revenir à la boîte de dialogue de fichier, pensez à Microsoft Word. Microsoft était libre de développer le format DOCX comme bon lui semblait dans certaines limites. Dans le même temps, Word lit ou écrit également des formats hérités et tiers (par exemple PDF). Ce programme n'est pas différent: le format "binaire" dont je parle est un format interne encore à définir conçu pour la vitesse. Dans le même temps, il doit être capable de lire et d'écrire des formats standard ouverts dans son domaine (sans rapport avec la question) afin de pouvoir travailler avec d'autres logiciels.
Enfin, il n'y a qu'un seul type de Widget. Il aura des objets enfants, mais ceux-ci seront gérés par cette logique de sérialisation. Le programme ne chargera jamais les widgets et les pignons. Cette conception ne doit concerner que les Widgets et les WidgetDatabases.