Estou escrevendo um aplicativo C ++. A maioria dos aplicativos lê e grava dados citados, e este não é uma exceção. Criei um design de alto nível para o modelo de dados e a lógica de serialização. Esta pergunta está solicitando uma revisão do meu design com esses objetivos específicos em mente:
Ter uma maneira fácil e flexível de ler e gravar modelos de dados em formatos arbitrários: binário bruto, XML, JSON, et. al. O formato dos dados deve ser dissociado dos próprios dados, bem como do código que está solicitando a serialização.
Garantir que a serialização seja o mais livre de erros possível. A E / S é inerentemente arriscada por vários motivos: meu design apresenta mais maneiras de falhar? Se sim, como eu poderia refatorar o design para mitigar esses riscos?
Este projeto usa C ++. Se você o ama ou odeia, a linguagem tem sua própria maneira de fazer as coisas e o design visa trabalhar com a linguagem, não contra ela .
Finalmente, o projeto é construído sobre os wxWidgets . Enquanto estou procurando uma solução aplicável a um caso mais geral, essa implementação específica deve funcionar bem com esse kit de ferramentas.
O que se segue é um conjunto muito simples de classes escritas em C ++ que ilustram o design. Essas não são as classes reais que escrevi parcialmente até agora; esse código simplesmente ilustra o design que estou usando.
Primeiro, alguns DAOs de amostra:
#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;
};
Em seguida, defino classes virtuais puras (interfaces) para leitura e gravação de DAOs. A idéia é abstrair a serialização de dados a partir dos próprios dados ( 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, aqui está o código que obtém o leitor / gravador adequado para o tipo de E / S desejado. Haveria subclasses de leitores / escritores também definidas, mas elas não acrescentam nada à revisão do 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 */);
}
};
Pelos objetivos declarados do meu projeto, tenho uma preocupação específica. Os fluxos C ++ podem ser abertos no modo texto ou binário, mas não há como verificar um fluxo já aberto. Poderia ser possível, por erro do programador, fornecer, por exemplo, um fluxo binário para um leitor / gravador XML ou JSON. Isso pode causar erros sutis (ou não tão sutis). Prefiro que o código falhe rapidamente, mas não tenho certeza de que esse design faria isso.
Uma maneira de contornar isso poderia ser descarregar a responsabilidade de abrir o fluxo para o leitor ou escritor, mas acredito que isso viola o SRP e tornaria o código mais complexo. Ao escrever um DAO, o gravador não deve se preocupar com a direção do fluxo: pode ser um arquivo, saída padrão, uma resposta HTTP, um soquete, qualquer coisa. Uma vez que essa preocupação é encapsulada na lógica de serialização, ela se torna muito mais complexa: ela deve conhecer o tipo específico de fluxo e qual construtor chamar.
Além dessa opção, não tenho certeza de qual seria a melhor maneira de modelar esses objetos, que é simples, flexível e ajuda a evitar erros lógicos no código que o utiliza.
O caso de uso com o qual a solução deve ser integrada é uma caixa de diálogo simples de seleção de arquivo . O usuário seleciona "Abrir ..." ou "Salvar como ..." no menu Arquivo, e o programa abre ou salva o WidgetDatabase. Também haverá opções "Importar ..." e "Exportar ..." para Widgets individuais.
Quando o usuário seleciona um arquivo para abrir ou salvar, o wxWidgets retornará um nome de arquivo. O manipulador que responde a esse evento deve ser um código de uso geral que aceite o nome do arquivo, adquira um serializador e chame uma função para realizar o trabalho pesado. Idealmente, esse design também funcionaria se outro pedaço de código estivesse executando E / S não-arquivo, como enviar um WidgetDatabase para um dispositivo móvel por um soquete.
Um widget salva em seu próprio formato? Interopera com os formatos existentes? Sim! Tudo acima. Voltando à caixa de diálogo do arquivo, pense no Microsoft Word. A Microsoft estava livre para desenvolver o formato DOCX como quisesse, dentro de certas restrições. Ao mesmo tempo, o Word também lê ou grava formatos legados e de terceiros (por exemplo, PDF). Este programa não é diferente: o formato "binário" de que falo é um formato interno ainda a ser definido, projetado para acelerar. Ao mesmo tempo, ele deve ser capaz de ler e gravar formatos padrão abertos em seu domínio (irrelevantes para a pergunta) para poder trabalhar com outro software.
Finalmente, existe apenas um tipo de widget. Ele terá objetos filhos, mas esses serão tratados por essa lógica de serialização. O programa nunca carregará Widgets e Sprockets. Este projeto única precisa se preocupar com Widgets e WidgetDatabases.