Огляд дизайну серіалізації C ++


9

Я пишу заявку на C ++. Більшість додатків читають і записують цитування даних, і це не є винятком. Я створив дизайн високого рівня для моделі даних та логіки серіалізації. Це питання вимагає переглянути мій дизайн з урахуванням наступних конкретних цілей:

  • Мати простий та гнучкий спосіб зчитування та запису моделей даних у довільних форматах: бінарний сировина, 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, сокет і все, що завгодно. Після того, як ця проблема вкладається в логіку серіалізації, вона стає набагато складнішою: вона повинна знати конкретний тип потоку та якого конструктора викликати.

Окрім цього варіанту, я не впевнений, що був би кращим способом моделювання цих об’єктів, який є простим, гнучким та допомагає запобігти логічним помилкам у коді, який його використовує.


Випадок використання, з яким рішення має бути інтегровано, - це просте діалогове вікно вибору файлів . Користувач вибирає "Відкрити ..." або "Зберегти як ..." з меню Файл, і програма відкриває або зберігає базу даних WidgetData. Будуть також опції "Імпорт ..." та "Експорт ..." для окремих віджетів.

Коли користувач вибере файл для відкриття або збереження, wxWidgets поверне ім'я файлу. Обробник, який реагує на цю подію, повинен бути кодом загального призначення, який приймає ім'я файлу, отримує серіалізатор і викликає функцію для важкого підйому. В ідеалі ця конструкція також буде працювати, якщо інший фрагмент коду виконує нефайлові введення / виведення, наприклад, надсилання бази WidgetDatabase на мобільний пристрій через сокет.


Чи віджет зберігає у власному форматі? Чи взаємодіє він із існуючими форматами? Так! Все вищеперераховане. Повертаючись до діалогового вікна файлів, подумайте про Microsoft Word. Майкрософт міг вільно розробляти формат DOCX, однак хотів у певних обмеженнях. У той же час Word також читає або записує застарілі та сторонні формати (наприклад, PDF). Ця програма не відрізняється: "бінарний" формат, про який я говорю, - це ще визначений внутрішній формат, розроблений для швидкості. У той же час він повинен вміти читати та записувати відкриті стандартні формати у своїй області (не має значення для питання), щоб він міг працювати з іншим програмним забезпеченням.

Нарешті, є лише один тип віджету. У нього будуть дочірні об’єкти, але вони будуть оброблятися цією логікою серіалізації. Програма ніколи не завантажує і віджети, і зірочки. Цей проект тільки потрібно мати справу з віджетами і WidgetDatabases.


1
Чи розглядали ви для цього використання бібліотеки Boost Serialization ? Він включає всі цілі дизайну, які у вас є.
Барт ван Інґен Шенау

1
@BartvanIngenSchenau у мене не було, в основному через стосунки кохання / ненависті, які я маю з Boost. Я думаю, що в цьому випадку деякі формати, які мені потрібні для підтримки, можуть бути складнішими, ніж Boost Serialization може працювати, не додаючи достатньої складності, що використання його не купує мене багато.

Ах! Отже, ви не є (де-) серіалізаційними примірниками віджетів (це було б дивно ...), але цим віджетам просто потрібно читати та записувати структуровані дані? Чи потрібно реалізувати існуючі формати файлів, чи ви можете визначити спеціальний формат? Чи використовують різні віджети загальні чи подібні формати, які можна реалізувати як загальну модель? Тоді ви можете зробити розділення користувальницького інтерфейсу - доменної логіки - моделі - DAL, а не змінювати все разом як об'єкт бога WxWidget. Насправді я не бачу, чому віджети тут доречні.
amon

@amon Я знову відредагував це питання. wxWidgets стосуються лише інтерфейсу з користувачем: віджети, про які я говорю, не мають нічого спільного з рамкою wxWidgets (тобто немає жодного об'єкта). Я просто використовую цей термін як загальну назву для типу DAO.

1
@LarsViklund ви висловлюєте переконливий аргумент і ви змінили мою думку з цього приводу. Я оновив приклад коду.

Відповіді:


7

Я можу помилятися, але ваш дизайн здається жахливо переосмисленим. Для сериализации тільки один Widget, ви хочете визначити WidgetReader, WidgetWriter, WidgetDatabaseReader, WidgetDatabaseWriterінтерфейси, кожна з яких має реалізацій для XML, JSON і бінарних кодувань, і завод , щоб зв'язати всі ці класи разом. Це проблематично з наступних причин:

  • Якщо я хочу , щоб серіалізовать не- Widgetклас, давайте назвемо це Foo, я повинен перевизначати весь цей зоопарк класів, а також створювати FooReader, FooWriter, FooDatabaseReader, FooDatabaseWriterінтерфейси, раз три для кожного формату сериализации, а також завод , щоб зробити його навіть віддалено використовувати. Не кажіть мені, що там не буде жодної копії та вставлення! Цей комбінаторний вибух видається досить незрозумілим, навіть якщо кожен із цих класів по суті містить лише один метод.

  • Widgetне може бути розумно капсульований. Або ви відкриєте все, що має бути серіалізовано до відкритого світу методами getter, або вам доведеться до friendкожної WidgetWriter(і, мабуть, і всі WidgetReader) реалізації. У будь-якому випадку ви введете значну зв’язок між реалізаціями серіалізації та Widget.

  • Читач / письменник зоопарк запрошує невідповідності. Кожного разу, коли ви додаєте учасника Widget, вам доведеться оновлювати всі відповідні класи серіалізації для зберігання / отримання цього члена. Це те, що неможливо статично перевірити на правильність, тому вам також доведеться написати окремий тест для кожного читача та письменника. У вашому поточному дизайні це 4 * 3 = 12 тестів на клас, який ви хочете серіалізувати.

    В іншому напрямку додавання нового формату серіалізації, такого як YAML, також є проблематичним. Для кожного класу, який ви хочете серіалізувати, вам доведеться пам’ятати про те, щоб додати YAML-зчитувач та письменник, і додати цей випадок до перерахунку та до фабрики. Знову ж таки, це неможливо статично перевірити, якщо ви не зробите (занадто) розумним і не складете шаблонний інтерфейс для фабрик, незалежних від цього, Widgetі забезпечите реалізацію для кожного типу серіалізації для кожної операції вводу / виходу.

  • Можливо, Widgetзараз задовольняє СРП, оскільки він не несе відповідальності за серіалізацію. Але реалізація читача та письменника явно не робить, якщо інтерпретація "SRP = кожен об'єкт має одну причину для зміни": реалізації повинні змінюватися, коли змінюється або формат серіалізації, або коли Widgetзмінюються.

Якщо ви зможете заздалегідь вкласти мінімум часу, будь ласка, спробуйте скласти більш загальну рамку серіалізації, ніж цей спеціальний клубок класів. Наприклад, ви можете визначити загальне представлення обміну, назвемо його SerializationInfo, як об'єктову модель, схожу на JavaScript: більшість об'єктів можна розглядати як std::map<std::string, SerializationInfo>, або як std::vector<SerializationInfo>, або як примітив, наприклад int.

Для кожного формату серіалізації у вас буде один клас, який керує зчитуванням і записом представлення серіалізації з цього потоку. І для кожного класу, який ви хочете серіалізувати, ви мали б якийсь механізм, який перетворює екземпляри з / у представлення серіалізації.

Я відчував таку конструкцію з cxxtools ( домашня сторінка , GitHub , демонстрація серіалізації ), і це здебільшого надзвичайно інтуїтивно, широко застосовно і задовільно для моїх випадків використання - єдиними проблемами є досить слабка об'єктна модель представлення серіалізації, яка вимагає від вас знати під час десеріалізації точно, який тип об’єкта ви очікуєте, і що дезаріалізація передбачає об'єкти, які можна сконструювати за замовчуванням, які можна ініціалізувати пізніше. Ось надуманий приклад використання:

class Point {
  int _x;
  int _y;
public:
  Point(x, y) : _x(x), _y(y) {}
  int x() const { return _x; }
  int y() const { return _y; }
};

void operator <<= (SerializationInfo& si, const Point& p) {
  si.addMember("x") <<= p.x();
  si.addMember("y") <<= p.y();
}

void operator >>= (const SerializationInfo& si, Point& p) {
  int x;
  si.getMember("x") >>= x;  // will throw if x entry not found
  int y;
  si.getMember("y") >>= y;
  p = Point(x, y);
}

int main() {
  // cxxtools::Json<T>(T&) wrapper sets up a SerializationInfo and manages Json I/O
  // wrappers for other formats also exist, e.g. cxxtools::Xml<T>(T&)

  Point a(42, -15);
  std::cout << cxxtools::Json(a);
  ...
  Point b(0, 0);
  std::cin >> cxxtools::Json(p);
}

Я не кажу, що ви повинні використовувати cxxtools або точно копіювати цей дизайн, але, на мій досвід, його дизайн робить тривіальним додавання серіалізації навіть для невеликих одноразових класів, за умови, що ви не надто дбаєте про формат серіалізації ( наприклад, вихід XML за замовчуванням буде використовувати імена членів як імена елементів і ніколи не використовуватиме атрибути для ваших даних).

Проблема з бінарним / текстовим режимом для потоків не видається вирішуваною, але це не так вже й погано. По-перше, це важливо лише для бінарних форматів, на платформах, на які я не схильний програмувати ;-) Більш серйозно, це обмеження вашої інфраструктури серіалізації, вам просто доведеться документувати та сподіваюся, що всі користуються правильно. Відкриття потоків у ваших читачах чи авторах є надто негнучким, і C ++ не має вбудованого механізму рівня типу, щоб відрізняти текст від бінарних даних.


Як би змінилася ваша порада, враховуючи, що ці DAO в основному вже є класом "інформація про серіалізацію"? Це C ++ еквівалент POJO . Я також збираюсь відредагувати своє запитання з трохи більше інформації про те, як ці об’єкти будуть використовуватися.
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.