Я пишу приложение на C ++. Большинству приложений требуется чтение и запись данных, и это не исключение. Я создал дизайн высокого уровня для модели данных и логики сериализации. Этот вопрос требует пересмотра моего дизайна с учетом этих конкретных целей:
Иметь простой и гибкий способ чтения и записи моделей данных в произвольных форматах: raw двоичный, XML, JSON и др. и др. Формат данных должен быть отделен от самих данных, а также от кода, который запрашивает сериализацию.
Чтобы обеспечить максимально безошибочную сериализацию. Ввод / вывод по своей природе сопряжен с риском по ряду причин: вводит ли мой дизайн больше способов его провала? Если так, как я мог бы реорганизовать дизайн, чтобы уменьшить эти риски?
Этот проект использует C ++. Любите ли вы это или ненавидите, у языка есть свой собственный способ действий, и дизайн нацелен на работу с языком, а не против него .
Наконец, проект построен поверх wxWidgets . Хотя я ищу решение, применимое к более общему случаю, эта конкретная реализация должна прекрасно работать с этим инструментарием.
Далее следует очень простой набор классов, написанных на C ++, которые иллюстрируют дизайн. Это не те классы, которые я частично написал до сих пор, этот код просто иллюстрирует дизайн, который я использую.
Во-первых, некоторые примеры 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;
};
Далее я определяю чисто виртуальные классы (интерфейсы) для чтения и записи DAO. Идея состоит в том, чтобы абстрагировать сериализацию данных от самих данных ( 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;
};
Наконец, вот код, который получает правильное устройство чтения / записи для нужного типа ввода / вывода. Также будут определены подклассы читателей / писателей, но они ничего не добавляют к обзору проекта:
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 */);
}
};
В соответствии с заявленными целями моего дизайна, у меня есть одна конкретная проблема. Потоки C ++ могут быть открыты в текстовом или двоичном режиме, но нет способа проверить уже открытый поток. Возможно из-за ошибки программиста предоставить, например, двоичный поток для чтения / записи XML или JSON. Это может вызвать тонкие (или не очень) ошибки. Я бы предпочел, чтобы код не работал быстро, но я не уверен, что этот дизайн сделает это.
Одним из способов решения этой проблемы может быть снятие ответственности за открытие потока для читателя или автора, но я считаю, что это нарушает SRP и сделает код более сложным. При написании DAO автору записи не нужно заботиться о том, куда движется поток: это может быть файл, стандартный вывод, ответ HTTP, сокет, что угодно. Как только эта проблема инкапсулируется в логике сериализации, она становится гораздо более сложной: она должна знать конкретный тип потока и какой конструктор вызывать.
Помимо этой опции, я не уверен, что будет лучшим способом моделирования этих объектов, который будет простым, гибким и поможет предотвратить логические ошибки в коде, который его использует.
Вариант использования, с которым необходимо интегрировать решение, - это простое диалоговое окно выбора файла . Пользователь выбирает «Открыть ...» или «Сохранить как ...» в меню «Файл», и программа открывает или сохраняет базу данных WidgetDatabase. Также будут доступны опции «Импорт ...» и «Экспорт ...» для отдельных виджетов.
Когда пользователь выбирает файл для открытия или сохранения, wxWidgets возвращает имя файла. Обработчик, который отвечает на это событие, должен быть кодом общего назначения, который принимает имя файла, получает сериализатор и вызывает функцию для выполнения тяжелой работы. В идеале этот дизайн также будет работать, если другой фрагмент кода выполняет нефайловый ввод / вывод, например, отправку базы данных WidgetDD на мобильное устройство через сокет.
Сохраняет ли виджет свой собственный формат? Взаимодействует ли он с существующими форматами? Да! Все вышеперечисленное. Возвращаясь к диалогу файла, подумайте о Microsoft Word. Microsoft была свободна в разработке формата DOCX, однако они хотели в рамках определенных ограничений. В то же время Word также читает или пишет устаревшие и сторонние форматы (например, PDF). Эта программа ничем не отличается: «двоичный» формат, о котором я говорю, - это еще не определенный внутренний формат, разработанный для скорости. В то же время он должен уметь читать и записывать открытые стандартные форматы в своей области (независимо от вопроса), чтобы он мог работать с другим программным обеспечением.
Наконец, есть только один тип виджетов. У него будут дочерние объекты, но они будут обрабатываться этой логикой сериализации. Программа никогда не загрузит как виджеты, так и звездочки. Этот проект только нужно иметь дело с виджетами и WidgetDatabases.
Ответы:
Я могу ошибаться, но ваш дизайн, кажется, ужасно перегружен. Для сериализации только один
Widget
, вы хотите определитьWidgetReader
,WidgetWriter
,WidgetDatabaseReader
,WidgetDatabaseWriter
интерфейсы, каждая из которых имеет реализаций для XML, JSON и бинарных кодировок, и завод , чтобы связать все эти классы вместе. Это проблематично по следующим причинам:Если я хочу , чтобы сериализовать не-
Widget
класс, давайте назовем этоFoo
, я должен переопределять весь этот зоопарк классов, а также создаватьFooReader
,FooWriter
,FooDatabaseReader
,FooDatabaseWriter
интерфейсы, раз три для каждого формата сериализации, а также завод , чтобы сделать его даже удаленно использовать. Не говорите мне, что там не будет копий и вставок! Этот комбинаторный взрыв кажется довольно неосуществимым, даже если каждый из этих классов по существу содержит только один метод.Widget
не может быть разумно заключен в капсулу. Либо вы открываете все, что должно быть сериализовано, в открытый мир с помощью методов-геттеров, либо у вас естьfriend
всеWidgetWriter
(и, вероятно, также всеWidgetReader
) реализации. В любом случае вы введете значительную связь между реализациями сериализации иWidget
.Читатель / писатель зоопарк предлагает несоответствия. Всякий раз, когда вы добавляете члена
Widget
, вам нужно будет обновить все связанные классы сериализации, чтобы сохранить / извлечь этого члена. Это то, что не может быть статически проверено на корректность, поэтому вам также придется написать отдельный тест для каждого читателя и писателя. В вашем текущем дизайне это 4 * 3 = 12 тестов на класс, который вы хотите сериализовать.В другом направлении добавление нового формата сериализации, такого как YAML, также проблематично. Для каждого класса, который вы хотите сериализовать, вы должны будете не забыть добавить читателя и писателя YAML и добавить этот случай в enum и на фабрику. Опять же, это то, что не может быть подвергнуто статическому тестированию, если вы не станете (слишком) умным и не создадите шаблонный интерфейс для фабрик, которые не зависят
Widget
и не обеспечат реализацию для каждого типа сериализации для каждой операции ввода / вывода.Может быть,
Widget
теперь удовлетворяет SRP, поскольку он не отвечает за сериализацию. Но реализации читателя и писателя явно не совпадают с интерпретацией «SRP = каждый объект имеет одну причину для изменения»: реализации должны измениться, когда либо изменяется формат сериализации, либо когдаWidget
изменения.Если вы можете потратить минимум времени заранее, попробуйте разработать более общую платформу сериализации, чем эта специальная путаница классов. Например, вы можете определить общее представление обмена, скажем так
SerializationInfo
, с помощью JavaScript-подобной объектной модели: большинство объектов можно рассматривать какstd::map<std::string, SerializationInfo>
, или какstd::vector<SerializationInfo>
, или как примитив, такой какint
.Для каждого формата сериализации у вас будет один класс, который управляет чтением и записью представления сериализации из этого потока. И для каждого класса, который вы хотите сериализовать, у вас будет какой-то механизм, который преобразует экземпляры из / в представление сериализации.
Я имел такой дизайн с cxxtools ( домашняя страница , GitHub , демонстрация сериализации ), и он в основном чрезвычайно интуитивно понятен, широко применим и удовлетворителен для моих случаев использования - единственными проблемами является довольно слабая объектная модель представления сериализации, которая требует от вас точно знать во время десериализации, какой тип объекта вы ожидаете, и что десериализация подразумевает конструируемые по умолчанию объекты, которые могут быть инициализированы позже. Вот надуманный пример использования:
Я не говорю, что вы должны использовать cxxtools или точно копировать этот дизайн, но, по моему опыту, его дизайн делает тривиальным добавление сериализации даже для небольших одноразовых классов при условии, что вам не слишком важен формат сериализации ( например, вывод XML по умолчанию будет использовать имена членов в качестве имен элементов и никогда не будет использовать атрибуты для ваших данных).
Проблема с двоичным / текстовым режимом для потоков не кажется разрешимой, но это не так уж плохо. С одной стороны, это имеет значение только для двоичных форматов, на платформах, для которых я не склонен программировать ;-) Более серьезно, это ограничение вашей инфраструктуры сериализации, которое вам просто нужно документировать и надеяться, что все используют правильно. Открытие потоков внутри ваших читателей или авторов слишком негибко, и в C ++ нет встроенного механизма на уровне типов, который бы отличал текст от двоичных данных.
источник