Cериализация для начинающих - Часть 3

ОГЛАВЛЕНИЕ

Это руководство описывает, как упорядочивать сложные объекты.

Данная статья является третьей частью руководства по сериализации, состоящего из трех частей.

  • Часть 1 является введением в основы сериализации.
  • Часть 2 объясняет, как обрабатывать чтение данных из неисправных хранилищ данных и поддерживать контроль версий.
  • Часть 3 описывает, как переводить в последовательную форму сложные объекты.

Сериализация производного класса

В данном случае производный класс является производным от простого класса и не является коллекцией. Для сериализации производного класса нужно сделать следующее:

  1. Сериализовать сигнатуру и версию объекта
  2. Сериализовать базовый класс объекта << дополнительный шаг
  3. Сериализовать элементы (члены) объекта (если они есть)

В следующем примере класс ColoredPoint является производным от класса Point и содержит дополнительный целочисленный член (int), названный m_nColor, который определяет цвет точки. Как и все упорядочиваемые классы, ColoredPoint также определяет статическую сигнатуру и версию.

  int ColoredPoint::serialize

    (CArchive* pArchive)
  {
    ASSERT (pArchive != NULL);
    // Шаг 1: Сериализация сигнатуры и версии
    int nVersion;
    try {
      if (pArchive->IsStoring()) {
          (*pArchive) << ColoredPoint::m_strSignature;
          (*pArchive) << ColoredPoint::m_nVersion;
      } else {
          CString strSignature;
          (*pArchive) >> strSignature;
          if (strSignature != ColoredPoint::m_strSignature)
             return (Status::InvalidFormat);
          (*pArchive) >> nVersion;
          if (nVersion > ColoredPoint::m_nVersion;)
             return (Status::UnsupportedVersion);
      }

      // Шаг 2: Сериализация базового класса
      int nStatus = Point::serialize (pArchive);
      if (nStatus != Status::Success)
         return (nStatus);

      // Шаг 3: Сериализация членов
      if (pArchive->IsStoring())
         (*pArchive) << m_nColor;
      else
         (*pArchive) >> m_nColor;

    }

    catch (CException* pException) {
      // Произошла ошибка чтения/записи
      pException->Delete();
      if (pArchive->IsStoring())
        return (Status::WriteError);
      return (Status::ReadError);

    }

    // Объект был успешно сериализован (упорядочен)
    return (Status::Success);

  }


Сериализация однородной коллекции

Однородные коллекции используются для хранения коллекций с динамически изменяемым размером, содержащих объекты одинакового типа. Для сериализации однородной коллекции нужно сделать следующее:

  1. Сериализовать сигнатуру и версию объекта
  2. Сериализовать базовый класс объекта (если он есть)
  3. Сериализовать число элементов в коллекции << дополнительный шаг
  4. Сериализовать каждый объект в коллекции << дополнительный шаг
  5. Сериализовать элементы (члены) объекта (если они есть)

В следующем примере класс ColoredPointList является коллекцией объектов ColoredPoint. Для простоты ColoredPointList использует CPtrArray для хранения объектов. Как и все упорядочиваемые классы, ColoredPointList также определяет статическую сигнатуру и версию. Здесь представлено как ColoredPointList: выглядит

   class ColoredPointList

  {
    // Конструктор/Деструктор
    public:
      ColoredPointList::ColoredPointList();
      virtual ColoredPointList::~ColoredPointList() 

    // Атрибуты
    public:
      static const CString m_strSignature;
      static const int m_nVersion;

    // Операции
    public:
      int serialize (CArchive* pArchive);
    // Члены
    protected:
      CPtrArray m_coloredPoints;  }

Здесь показана его сериализация:

   int ColoredPointList::serialize

    (CArchive* pArchive)
  {
    ASSERT (pArchive != NULL);
    int nStatus = Status::Success;
 
    // Шаг 1: Сериализация сигнатуры и версии
    int nVersion;
    try {
      if (pArchive->IsStoring()) {
          (*pArchive) << ColoredPointList::m_strSignature;
          (*pArchive) << ColoredPointList::m_nVersion;

      } else {
          CString strSignature;
          (*pArchive) >> strSignature;
          if (strSignature != ColoredPointList::m_strSignature)
             return (Status::InvalidFormat);
          (*pArchive) >> nVersion;
          if (nVersion > ColoredPointList::m_nVersion;)
             return (Status::UnsupportedVersion);
      }

      // Шаг 2: Сериализация базового класса (если он есть)
      //
      // Ничего не выполняется, так как ColoredPointList не является производным классом.
      // Но если бы он был произведен от BaseColoredPointList, то выполнялся бы ряд действий:
      //
      // nStatus = BaseColoredPointList::serialize (pArchive);
      // if (nStatus != Status::Success)
      //    return (nStatus);

      // Шаг 3: Сериализация числа элементов в коллекции
      int nItems = 0;
      if (pArchive->IsStoring()) {
           nItems = m_coloredPoints.GetSize();
           (*pArchive) << nItems;

      } else
         (*pArchive) >> nItems;

      // Шаг 4: Сериализация каждого объекта в коллекции
      for (int nObject=0; (nObject < nItems); nObject++) {

          // 4a: Сериализация указателя на объект

          ColoredPoint* pColoredPoint = NULL;
          if (pArchive->IsStoring())
             pColoredPoint = (ColoredPoint *) m_coloredPoints.GetAt (nObject);
          else
             pColoredPoint = new ColoredPoint();

          ASSERT (pColoredPoint != NULL);
 
          // 4b: Сериализация объекта
          nStatus = pColoredPoint->serialize (pArchive);

          if (nStatus != Status::Success)
             return (nStatus);
          if (!pArchive->IsStoring())
             m_coloredPoints.Add (pColoredPoint);

      }
 
      // Шаг 5: Сериализация других элементов объекта (если они есть)
      //
      // Ничего не делается, так как ColoredPointList не имеет никаких других элементов. Но если бы он содержал целочисленный элемент (m_nSomeInt) и объект Foo
      //  (m_foo), выполнялись бы следующие действия:
      //
      // if (pArchive->IsStoring())
      //    (*pArchive) << m_nSomeInt;
      // else
      //    (*pArchive) >> m_nColor;
      //
      // nStatus = m_foo::serialize (pArchive);
      // if (nStatus != Status::Success)
      //    return (nStatus);
    }

    catch (CException* pException) {
      // Произошла ошибка чтения/записи
      pException->Delete();
      if (pArchive->IsStoring())
        return (Status::WriteError);
      return (Status::ReadError);
    }

    // Объект был успешно сериализован (упорядочен)
    return (Status::Success);

  }


Сериализация разнородной коллекции

Разнородные коллекции используются для хранения коллекций динамически изменяемых размеров, содержащих потенциально различные типы объектов. Для сериализации разнородной коллекции необходимо сделать следующее:

  1. Сериализовать сигнатуру и версию объекта
  2. Сериализовать базовый класс объекта (если он есть)
  3. Сериализовать число элементов в коллекции
  4. Для каждого объекта в коллекции
    (a) сериализовать сигнатуру этого объекта << дополнительный шаг
    (b) затем сериализовать объект
  5. Сериализовать другие элементы объекта (если они есть)

Дополнительным шагом в сериализации разнородных коллекций является 4(a), где сериализуется сигнатура каждого объекта перед сериализацией самого объекта. Это оказывается полезным при обратном считывании данных. При сериализации однородной коллекции приходится иметь дело с объектами одинакового типа (ColoredPoint в предыдущем примере). Для чтения ColoredPoint этот класс создается в области кучи, и затем вызывается его метод serialize().

 ColoredPoint* pColoredPoint = new ColoredPoint();
  nStatus = pColoredPoint->serialize (pArchive);

Для разнородных коллекций необходимо знать тип каждого считываемого в обратном направлении объекта, перед тем как его сериализовать. Здесь и нужно использовать сигнатуру объекта. Поскольку сигнатура была сохранена на выходе, можно создать объект соответствующего типа на входе.

     // Чтение сигнатуры объекта
  CString strSignature;
  pArchive >> strSignature;

  // Создание объекта соответствующего типа
  ISerializable* pObject = NULL;
  if (strSignature == ColoredPoint::m_strSignature)
     pObject = new ColoredPoint();
  else
    if (strSignature == Line::m_strSignature)
       pObject = new Line();
    else
       if (strSignature == Rectangle::m_strSignature)
          pObject = new Rectangle();
       else
          return (Status::InvalidFormat);
  ASSERT (pObject != NULL);
  // Чтение из объекта в обратном направлении
  nStatus = pObject->serialize (pArchive);

В приведенном выше фрагменте кода ColoredPoint, Line и Rectangle - все являются производными от общего базового класса ISerializable, который является абстрактным базовым классом, содержащим только чисто виртуальные методы (другими словами, "интерфейс"). ISerializable определяет методы getSignature(), getVersion() и serialize().

  class ISerializable
  {
    // Конструктор/деструктор
    public:
      ISerializable::ISerializable()
        { }
      virtual ISerializable::~ISerializable()
        { }
    // Методы
    public:
      // Получение сигнатуры объекта
      virtual CString getSignature() = 0;
 
      // Получение версии объекта
      virtual int getVersion() = 0;

      // Сериализация объекта
      virtual int serialize (CArchive* pArchive) = 0;

  }

Теперь выполним сериализацию разнородной коллекции. В следующем примере класс ShapeList является коллекцией различных количеств объектов ColoredPoint, Line и Rectangle, все из которых являются производными от ISerializable. На эти классы можно смотреть как на "реализующие интерфейс ISerializable".

   int ShapeList::serialize

    (CArchive* pArchive)
  {
    ASSERT (pArchive != NULL);
    int nStatus = Status::Success;
    // Шаг 1: Сериализация сигнатуры и версии
    int nVersion;
    try {
      if (pArchive->IsStoring()) {
          (*pArchive) << ShapeList::m_strSignature;
          (*pArchive) << ShapeList::m_nVersion;
      } else {
          CString strSignature;
          (*pArchive) >> strSignature;
          if (strSignature != ShapeList::m_strSignature)
             return (Status::InvalidFormat);
          (*pArchive) >> nVersion;
          if (nVersion > ShapeList::m_nVersion;)
             return (Status::UnsupportedVersion);
      }

      // Шаг 2: Сериализация базового класса (если он есть)
      //
      // Ничего не выполняется, так как ShapeList не является производным классом.
      // Но если бы он был производным от BaseShapeList, выполнялись бы следующие действия:
      //
      // nStatus = BaseShapeList::serialize (pArchive);
      // if (nStatus != Status::Success)
      //    return (nStatus);

      // Шаг 3: Сериализация числа элементов в коллекции
      int nItems = 0;
      if (pArchive->IsStoring()) {
           nItems = m_shapes.GetSize();
           (*pArchive) << nItems;
      } else
           (*pArchive) >> nItems;
 
      // Шаг 4: Сериализация каждого элемента в коллекции
      for (int nObject=0; (nObject < nItems); nObject++) {

          // 4a: Сначала выполняется сериализация сигнатуры объекта
          CString strSignature;
          if (pArchive->IsStoring())
             (*pArchive) << pObject->getSignature();
          else
             (*pArchive) >> strSignature;

          //
          // 4b: Затем сериализуется объект
          //
 
          // 4b (1): Указатель на сериализуемый объект
          ISerializable* pObject = NULL;
          if (pArchive->IsStoring())

             pObject = (ISerializable *) m_shapes.GetAt (nObject);
          else {
             if (strSignature == ColoredPoint::m_strSignature)
                pObject = new ColoredPoint();

             else
               if (strSignature == Line::m_strSignature)
                  pObject = new Line();

               else
                  if (strSignature == Rectangle::m_strSignature)
                     pObject = new Rectangle();

                  else
                     return (Status::InvalidFormat);
          }

          ASSERT (pObject != NULL);
 
          // 4b (2): Сериализация объекта
          nStatus = pObject->serialize (pArchive);
          if (nStatus != Status::Success)
             return (nStatus);
          if (!pArchive->IsStoring())
             m_shapes.Add (pObject);
      }

      // Шаг 5: Сериализация других элементов объекта (если они есть)
      //
      // Ничего не делается, так как ShapeList не имеет никаких других членов.  Но если бы он содержал целое число (m_nSomeInt) и
      // объект Foo (m_foo), выполнялись бы следующие действия:
      //
      // if (pArchive->IsStoring())
      //    (*pArchive) << m_nSomeInt;
      // else
      //    (*pArchive) >> m_nColor;
      //
      // nStatus = m_foo::serialize (pArchive);
      // if (nStatus != Status::Success)
      //    return (nStatus);
    }

    catch (CException* pException) {
      // Произошла ошибка чтения/записи
      pException->Delete();
      if (pArchive->IsStoring())
        return (Status::WriteError);
      return (Status::ReadError);
    }

    // Объект был успешно сериализован (упорядочен)
    return (Status::Success);
  }


Фабрика класса

Вы можете заменить условный оператор во фрагменте кода, используя "фабрику класса" для создания нового члена класса на основе его сигнатуры.

Фабрика класса - это немного больше чем класс, предоставляющий соответственно названный статический метод (например: create()), который создает объект определенного типа. Вы можете скрыть условный оператор в методе фабрики create(), немного приведя код в порядок..

   ...
  // Создание объекта соответствующего типа
  ISerializable* pObject = MyClassFactory::create (strSignature);
  ASSERT (pObject != NULL);
  ...

Заключение

Хотя этот пример ссылается на объекты MFC, такие как CString и CPtrArray, сериализация не относится только к MFC. Это обычное действие, выполняемое любым программным обеспечением, которому требуется сохранять и восстанавливать информацию в и из постоянного хранилища.

Автор: Ravi Bhavnani