Estoy escribiendo una aplicación C ++. La mayoría de las aplicaciones leen y escriben datos citas de necesarias y esta no es una excepción. Creé un diseño de alto nivel para el modelo de datos y la lógica de serialización. Esta pregunta solicita una revisión de mi diseño con estos objetivos específicos en mente:
Tener una manera fácil y flexible de leer y escribir modelos de datos en formatos arbitrarios: binario sin formato, XML, JSON, et. Alabama. El formato de los datos se debe desacoplar de los datos en sí, así como del código que solicita la serialización.
Para garantizar que la serialización esté tan libre de errores como sea razonablemente posible. La E / S es intrínsecamente riesgosa por una variedad de razones: ¿mi diseño introduce más formas de que falle? Si es así, ¿cómo podría refactorizar el diseño para mitigar esos riesgos?
Este proyecto usa C ++. Ya sea que lo ames o lo odies, el lenguaje tiene su propia forma de hacer las cosas y el diseño tiene como objetivo trabajar con el lenguaje, no en contra de él .
Finalmente, el proyecto está construido sobre wxWidgets . Si bien estoy buscando una solución aplicable a un caso más general, esta implementación específica debería funcionar bien con ese kit de herramientas.
Lo que sigue es un conjunto muy simple de clases escritas en C ++ que ilustran el diseño. Estas no son las clases reales que he escrito parcialmente hasta ahora, este código simplemente ilustra el diseño que estoy usando.
Primero, algunos DAO de muestra:
#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;
};
A continuación, defino clases virtuales puras (interfaces) para leer y escribir DAO. La idea es abstraer la serialización de datos de los datos en sí ( 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;
};
Finalmente, aquí está el código que obtiene el lector / escritor adecuado para el tipo de E / S deseado. Habría subclases de los lectores / escritores también definidos, pero estos no agregan nada a la revisión del diseño:
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 */);
}
};
Según los objetivos establecidos de mi diseño, tengo una preocupación específica. Las secuencias de C ++ se pueden abrir en modo de texto o binario, pero no hay forma de verificar una secuencia ya abierta. A través del error del programador, podría ser posible proporcionar, por ejemplo, una secuencia binaria a un lector / escritor XML o JSON. Esto podría causar errores sutiles (o no tan sutiles). Preferiría que el código fallara rápidamente, pero no estoy seguro de que este diseño lo haga.
Una forma de evitar esto podría ser descargar la responsabilidad de abrir la transmisión al lector o escritor, pero creo que eso viola SRP y haría que el código sea más complejo. Al escribir un DAO, al escritor no le debe importar a dónde va la transmisión: podría ser un archivo, salida estándar, una respuesta HTTP, un socket, cualquier cosa. Una vez que esa preocupación se encapsula en la lógica de serialización, se vuelve mucho más compleja: debe conocer el tipo específico de flujo y a qué constructor llamar.
Aparte de esa opción, no estoy seguro de cuál sería una mejor manera de modelar estos objetos que sea simple, flexible y que ayude a evitar errores lógicos en el código que lo usa.
El caso de uso con el que debe integrarse la solución es un cuadro de diálogo de selección de archivo simple . El usuario selecciona "Abrir ..." o "Guardar como ..." en el menú Archivo, y el programa abre o guarda la WidgetDatabase. También habrá opciones "Importar ..." y "Exportar ..." para widgets individuales.
Cuando el usuario selecciona un archivo para abrir o guardar, wxWidgets devolverá un nombre de archivo. El controlador que responde a ese evento debe ser un código de propósito general que tome el nombre del archivo, adquiera un serializador y llame a una función para realizar el trabajo pesado. Idealmente, este diseño también funcionaría si otra pieza de código realiza E / S sin archivos, como enviar una WidgetDatabase a un dispositivo móvil a través de un zócalo.
¿Se guarda un widget en su propio formato? ¿Interopera con los formatos existentes? ¡Si! Todas las anteriores. Volviendo al diálogo de archivo, piense en Microsoft Word. Microsoft era libre de desarrollar el formato DOCX como quisiera dentro de ciertas limitaciones. Al mismo tiempo, Word también lee o escribe formatos heredados y de terceros (por ejemplo, PDF). Este programa no es diferente: el formato "binario" del que hablo es un formato interno aún por definir, diseñado para la velocidad. Al mismo tiempo, debe poder leer y escribir formatos estándar abiertos en su dominio (irrelevante para la pregunta) para poder trabajar con otro software.
Finalmente, solo hay un tipo de Widget. Tendrá objetos secundarios, pero estos serán manejados por esta lógica de serialización. El programa nunca cargará Widgets y Sprockets. Este diseño solo tiene que ver con Widgets y WidgetDatabases.
Respuestas:
Puedo estar equivocado, pero su diseño parece estar terriblemente diseñado en exceso. Para serializar sólo uno
Widget
, que desea definirWidgetReader
,WidgetWriter
,WidgetDatabaseReader
,WidgetDatabaseWriter
interfaces que tienen cada una de las implementaciones XML, JSON y codificaciones binarias, y una fábrica para atar todos esos clases juntos. Esto es problemático por las siguientes razones:Si quiero serializar un no
Widget
clase, vamos a llamarloFoo
, tengo que volver a implementar toda esta Zoológico de clases, y crearFooReader
,FooWriter
,FooDatabaseReader
,FooDatabaseWriter
interfaces, tres veces para cada formato de serialización, además de una fábrica para que sea aún remotamente utilizable. ¡No me digas que no habrá copiar y pegar allí! Esta explosión combinatoria parece ser bastante imposible de mantener, incluso si cada una de esas clases esencialmente solo contiene un método único.Widget
no se puede encapsular razonablemente. O abres todo lo que debería ser serializado al mundo abierto con métodos getter, o tienes que realizarfriend
todas y cada una deWidgetWriter
lasWidgetReader
implementaciones (y probablemente también todas ). En cualquier caso, introducirá un acoplamiento considerable entre las implementaciones de serialización y elWidget
.El zoológico lector / escritor invita a las inconsistencias. Cada vez que agregue un miembro
Widget
, deberá actualizar todas las clases de serialización relacionadas para almacenar / recuperar ese miembro. Esto es algo que no puede verificarse estáticamente para su corrección, por lo que también tendrá que escribir una prueba por separado para cada lector y escritor. En su diseño actual, eso es 4 * 3 = 12 pruebas por clase que desea serializar.En la otra dirección, agregar un nuevo formato de serialización como YAML también es problemático. Para cada clase que desee serializar, deberá recordar agregar un lector y escritor de YAML, y agregar ese caso a la enumeración y a la fábrica. Nuevamente, esto es algo que no se puede probar estáticamente, a menos que sea (también) inteligente y elabore una interfaz con plantilla para fábricas que sea independiente
Widget
y se asegure de que se proporcione una implementación para cada tipo de serialización para cada operación de entrada / salida.Quizás el
Widget
ahora satisface el SRP ya que no es responsable de la serialización. Pero las implementaciones de lector y escritor claramente no lo hacen, con la interpretación “SRP = cada objeto tiene una razón para cambiar”: las implementaciones deben cambiar cuando cambia el formato de serialización o cuando losWidget
cambios.Si puede invertir un mínimo de tiempo de antemano, intente elaborar un marco de serialización más genérico que esta maraña de clases ad-hoc. Por ejemplo, podría definir una representación de intercambio común, llamémosla
SerializationInfo
, con un modelo de objeto similar a JavaScript: la mayoría de los objetos se pueden ver como astd::map<std::string, SerializationInfo>
, o comostd::vector<SerializationInfo>
, o como un primitivo comoint
.Para cada formato de serialización, tendrá una clase que gestiona la lectura y escritura de una representación de serialización de esa secuencia. Y para cada clase que desee serializar, tendrá algún mecanismo que convierta las instancias de / a la representación de serialización.
He experimentado un diseño de este tipo con cxxtools ( página de inicio , GitHub , demostración de serialización ), y es en su mayoría extremadamente intuitivo, ampliamente aplicable y satisfactorio para mis casos de uso; los únicos problemas son el modelo de objeto bastante débil de la representación de serialización que requiere saber durante la deserialización con precisión qué tipo de objeto está esperando, y esa deserialización implica objetos construibles por defecto que se pueden inicializar más tarde. Aquí hay un ejemplo de uso artificial:
No estoy diciendo que deba usar cxxtools o copiar exactamente ese diseño, pero en mi experiencia su diseño hace que sea trivial agregar serialización incluso para clases pequeñas y únicas, siempre que no le importe demasiado el formato de serialización ( por ejemplo, la salida XML predeterminada usará nombres de miembros como nombres de elementos, y nunca usará atributos para sus datos).
El problema con el modo binario / texto para transmisiones no parece solucionable, pero eso no es tan malo. Por un lado, solo importa para formatos binarios, en plataformas para las que no tiendo a programar ;-) Más en serio, es una restricción de su infraestructura de serialización que solo tendrá que documentar y esperar que todos usen correctamente. Abrir las transmisiones dentro de sus lectores o escritores es demasiado inflexible, y C ++ no tiene un mecanismo de nivel de tipo incorporado para distinguir el texto de los datos binarios.
fuente