Ich schreibe eine C ++ - Anwendung. Die meisten Anwendungen lesen und schreiben Daten , und dies ist keine Ausnahme. Ich habe ein übergeordnetes Design für das Datenmodell und die Serialisierungslogik erstellt. Diese Frage erfordert eine Überprüfung meines Designs unter Berücksichtigung dieser spezifischen Ziele:
Einfache und flexible Möglichkeit zum Lesen und Schreiben von Datenmodellen in beliebigen Formaten: Raw Binary, XML, JSON, et. al. Das Datenformat sollte von den Daten selbst sowie dem Code, der die Serialisierung anfordert, entkoppelt werden.
Um sicherzustellen, dass die Serialisierung so fehlerfrei wie möglich ist. E / A ist aus verschiedenen Gründen von Natur aus riskant: Führt mein Design mehr Möglichkeiten zum Fehlschlagen ein? Wenn ja, wie könnte ich das Design umgestalten, um diese Risiken zu minimieren?
Dieses Projekt verwendet C ++. Ob Sie es lieben oder hassen, die Sprache hat ihre eigene Art, Dinge zu tun, und das Design zielt darauf ab, mit der Sprache zu arbeiten, nicht dagegen .
Schließlich baut das Projekt auf wxWidgets auf . Während ich nach einer Lösung suche, die auf einen allgemeineren Fall anwendbar ist, sollte diese spezifische Implementierung mit diesem Toolkit gut funktionieren.
Was folgt, ist eine sehr einfache Reihe von in C ++ geschriebenen Klassen, die das Design veranschaulichen. Dies sind nicht die tatsächlichen Klassen, die ich bisher teilweise geschrieben habe. Dieser Code veranschaulicht lediglich das von mir verwendete Design.
Zunächst einige Beispiel-DAOs:
#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;
};
Als nächstes definiere ich reine virtuelle Klassen (Schnittstellen) zum Lesen und Schreiben von DAOs. Die Idee ist, die Serialisierung von Daten von den Daten selbst ( SRP ) zu abstrahieren .
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;
};
Schließlich ist hier der Code, der den richtigen Leser / Schreiber für den gewünschten E / A-Typ erhält. Es würden auch Unterklassen der Leser / Autoren definiert, aber diese tragen nichts zur Entwurfsprüfung bei:
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 */);
}
};
Gemäß den angegebenen Zielen meines Entwurfs habe ich ein spezifisches Anliegen. C ++ - Streams können im Text- oder Binärmodus geöffnet werden, es gibt jedoch keine Möglichkeit, einen bereits geöffneten Stream zu überprüfen. Es könnte durch einen Programmiererfehler möglich sein, einem XML- oder JSON-Leser / -Schreiber beispielsweise einen Binärstrom bereitzustellen. Dies kann zu subtilen (oder nicht so subtilen) Fehlern führen. Ich würde es vorziehen, wenn der Code schnell ausfällt, aber ich bin mir nicht sicher, ob dieses Design dies tun würde.
Eine Möglichkeit, dies zu umgehen, könnte darin bestehen, die Verantwortung für das Öffnen des Streams für den Leser oder Schreiber zu verlagern, aber ich glaube, dass dies gegen SRP verstößt und den Code komplexer machen würde. Beim Schreiben eines DAO sollte sich der Writer nicht darum kümmern, wohin der Stream geht: Es kann sich um eine Datei, einen Standardausgang, eine HTTP-Antwort, einen Socket oder etwas anderes handeln. Sobald dieses Problem in der Serialisierungslogik enthalten ist, wird es weitaus komplexer: Es muss den spezifischen Stream-Typ und den aufzurufenden Konstruktor kennen.
Abgesehen von dieser Option bin ich mir nicht sicher, wie diese Objekte besser modelliert werden können. Dies ist einfach, flexibel und hilft, Logikfehler in dem Code zu vermeiden, der sie verwendet.
Der Anwendungsfall, in den die Lösung integriert werden muss, ist ein einfaches Dialogfeld zur Dateiauswahl . Der Benutzer wählt "Öffnen ..." oder "Speichern unter ..." aus dem Menü "Datei" und das Programm öffnet oder speichert die WidgetDatabase. Es gibt auch die Optionen "Importieren ..." und "Exportieren ..." für einzelne Widgets.
Wenn der Benutzer eine Datei zum Öffnen oder Speichern auswählt, gibt wxWidgets einen Dateinamen zurück. Der Handler, der auf dieses Ereignis reagiert, muss ein Allzweckcode sein, der den Dateinamen verwendet, einen Serializer abruft und eine Funktion aufruft, um das schwere Heben durchzuführen. Idealerweise funktioniert dieses Design auch, wenn ein anderer Code Nicht-Datei-E / A ausführt, z. B. das Senden einer WidgetDatabase über einen Socket an ein mobiles Gerät.
Speichert ein Widget in einem eigenen Format? Funktioniert es mit vorhandenen Formaten? Ja! Alles das oben Genannte. Denken Sie beim Zurückkehren zum Dateidialog an Microsoft Word. Microsoft konnte das DOCX-Format unter bestimmten Bedingungen nach Belieben entwickeln. Gleichzeitig liest oder schreibt Word auch Legacy- und Drittanbieterformate (z. B. PDF). Dieses Programm ist nicht anders: Das "binäre" Format, über das ich spreche, ist ein noch zu definierendes internes Format, das auf Geschwindigkeit ausgelegt ist. Gleichzeitig muss es in der Lage sein, offene Standardformate in seiner Domäne zu lesen und zu schreiben (für die Frage irrelevant), damit es mit anderer Software arbeiten kann.
Schließlich gibt es nur einen Widget-Typ. Es werden untergeordnete Objekte vorhanden sein, die jedoch von dieser Serialisierungslogik verarbeitet werden. Das Programm lädt niemals sowohl Widgets als auch Kettenräder. Dieser Entwurf nur muss mit Widgets und WidgetDatabases zur Beunruhigung.