应用程序框架设计(3):RuntimeClass与序列化

来源:互联网 发布:动态图加字软件 编辑:程序博客网 时间:2024/05/06 05:56

SW系统的根是SObject,顾名思义是对普遍意义上的对象的抽象。其主要的支持有:

  • 运行时刻类信息(RuntimeClass)

    运行时刻类信息是经典程序结构中一个极其重要的部分。MFC、VCL、OWL、TurboVision都支持运行时刻类信息。它可能也是经典Object类中唯一比较实用的东西。而同时它也是Object类最容易让人感到迷惑的地方。简单地说,运行时刻类信息主要有两个用途:

    a)创建对象
    b)确定对象的类型

    其实RuntimeClass的实现机制一点也不神秘。它无非是通过类注册方式将类名与其父类、实例创建函数联系起来。

    SW系统的运行时刻类信息定义为:
    typedef SObject * (*FNBUILDER)();
    struct SRuntimeClass
    {
    LPCTSTR m_lpszClassName;
    // 类名
    SRuntimeClass * m_lpBaseClass;// 父类
    FNBUILDER m_fnCreator;// 实例创建
    SRuntimeClass *m_lpPrevClass;
    };

    每一个SW系统的类都对应有一个SRuntimeClass实例来描述该类。可以用__typeid(Class)来找到定位该类RuntimeClass信息。此宏与MFC的RUNTIME_CLASS(Class)宏完全相同。

    注意到SRuntimeClass有一个成员m_lpPrevClass,它只是用于在系统维护RuntimeClass链表。从而可以让用户通过类的名字来查询一个类的运行时刻类信息,并且创建该类的对象。例如下面的序列化技术就需要这样做。

    SW系统的运行时刻类信息相关函数和宏主要有:

    __typeid(Class)// 定位一个类的运行时刻类信息
    DECLARE_CLASS(Class)// 声明一个类
    IMPLEMENT_CLASS(Class, BaseClass)// 注册一个类

    SObject 
    * gCreateObject(LPCTSTR szClassName);
        
    // 根据一个类的类名,创建该类的一个实例
    const SRuntimeClass* SObject::GetRuntimeClass() const;
        
    // 确定一个对象的RuntimeClass信息
    BOOL SObject::IsKindOf(const SRuntimeClass *pRC) const;
        
    // 判断一个对象是否是pRC描述的类或者其派生类的实例

    它们的实现代码相当简单,不详细解释:

    #define __typeid(Class)    (&Class::x_theRuntimeClass)

    #define DECLARE_CLASS(Class) 
        
    const SRuntimeClass *Class::GetRuntimeClass() const
        
    static const SRuntimeClass x_theRuntimeClass; 
        
    static SObject *x_CreateObject();

    #define IMPLEMENT_CLASS(Class, BaseClass) 
    const SRuntimeClass *Class::GetRuntimeClass() const 

        
    return __typeid(Class); 

    const SRuntimeClass Class::x_theRuntimeClass( 
        _T(#Class),
        __typeid(BaseClass),
        Class::x_CreateObject
        );
    SObject 
    *Class::x_CreateObject()
    {
        return new Class;
    }

    const SRuntimeClass *x_lpRuntimeClassListHead = NULL;

    SRuntimeClass::SRuntimeClass(
        LPCTSTR szClassName, 
        
    const SRuntimeClass *lpBaseClass,
        FNBUILDER fnCreator)
    {
        m_szClassName 
    = szClassName;
        m_lpBaseClass 
    = lpBaseClass;
        m_fnCreator 
    = fnCreator;
        m_lpPrevClass 
    = x_lpRuntimeClassListHead;
        x_lpRuntimeClassListHead 
    = this;
    }

    SObject 
    *gCreateObject(LPCTSTR szClassName)
    {
        
    const SRuntimeClass *pRC = x_lpRuntimeClassListHead;
        
    for ( ; pRC; pRC = pRC->m_lpPrevClass)
            
    if (!_tcscmp(szClassName, pRC->m_szClassName))
                
    return pRC->fnCreateObject();
        
    return NULL;
    }

    BOOL SObject::IsKindOf(
    const SRuntimeClass *pBaseClass) const
    {
        SRuntimeClass 
    *pRC = this->GetRuntimeClass();
        
    for ( ; pRC; pRC = pRC->m_lpBaseClass)
        
    if (pRC == pBaseClass)
            
    return TRUE;
        
    return FALSE;
    }
  • 序列化(Serialization)

    序列化是建立在运行时刻类信息(RuntimeClass)之上的一个应用。所谓序列化是指通过一个自动化机制将对象保存到磁盘,或者将对象从磁盘读出来。尽管序列化有种种的缺点,但不得不承认它是对面相对象思想的一个经典运用。如果你的程序支持序列化,那么存盘的过程你唯一要做的就是,你对应用程序说,“存盘!”而后应用程序就可以将自己保存到磁盘上。你对应用程序说,“读盘!”而后应用程序就会从磁盘中读入数据重建对象。一切就这么简单!

    但是序列化是有严重缺陷的。它没有在兼容性、容错性上的保证。所以它比较适合于保存不被推广的文件格式,例如程序配置,它们可能不必考虑兼容性问题。但在对用户数据的保存上,它应该只是一个理论上的成果。所以尽管Microsoft提供了序列化,但是他自己从来都不会使用序列化去保存数据文件。

    SW系统的序列化实现上与MFC完全一致。但它是在当我还在还没有接触MFC时就已经实现的一个技术。这种一致性应该说是对面向对象思想把握的必然结果。但前提是你掌握了RuntimeClass(在实现RuntimeClass上,我主要借鉴了TurboVision中的实现方式。但是对其进行了大量的简化)。

    面向对象的基础思想是,当你要一个对象做一件事时,你只要向它发送相应的消息,而不必关心对象如何完成此任务。同样地,在接收到用户的存盘消息后,你只是简单地向应用程序发送存盘消息。而应用程序在保存完私有数据后继续向其所有子对象发送存盘消息,这个过程一直延续到简单对象。从而完成存盘动作。当然,为了支持序列化,SObject类要加虚函数:
        virtual Serialize(SArchive &ar);
    其中SArchive类是流操作类。与C++的流操作类基本类似,但它是二进制流。

    在实现序列化中,实现写盘是简单的:
        HRESULT SArchive::WriteObject(SObject *pOb);

    主要需要解决的问题时读盘时对象的重建上。一般地,读盘时会有这样两种需求:

    a)需要读取的对象已经被分配内存(即对象已经存在)。对应的读盘函数为:
        HRESULT SArchive::ReadObject(SObject *pOb);

    b)对象的类型未知,从而对象不可能预先创建。对应读盘函数为:
        HRESULT SArchive::ReadObject(
            const SRuntimeClass *pClassRequest,
            SObject **ppOb);

    情形a)是比较简单的,它不需要RuntimeClass的支持。MFC中没有提供此函数。现考虑情形b)。为了能够从磁盘重建对象,显然应该将一个描述对象的id值与对象的创建函数关联。这已经由RuntimeClass技术完成了。在对象id的选取上,SW系统与MFC都使用了对象的类名。但是这不是唯一的选择。这一点在后面的COM技术部分还会提到。不管怎样,我们可以说,为了能够从磁盘中重建对象,需要在保存对象具体数据之前先保存对象的类信息(RuntimeClass)。读盘时先读出类信息,由此重建对象实例。代码如下:
    void SArchive::WriteObject(SObject *pOb)
    {
        WriteString(pOb
    ->GetRuntimeClass()->m_szClassName);
        pOb
    ->Write(*this);
    }

    对于情形a)的ReadObject:
    HRESULT SArchive::ReadObject(SObject *pOb)
    {
        LPCTSTR lpsz 
    = ReadString();
        
    if (!_tcscmp(pOb->GetRuntimeClass()->m_szClassName, lpsz))
        {
            delete[] lpsz;
            pOb
    ->Read(*this);
            
    return S_OK;
        }
        delete[] lpsz;
        
    return E_FAIL;
    }

    对于情形b)的ReadObject:
    HRESULT SArchive::ReadObject(
        
    const SRuntimeClass *pClassReq,
        SObject 
    **ppOb)
    {
        LPCTSTR szReadedClass 
    = ReadString();
        
    *ppOb = NULL;
        
    if (pClassReq)
        {
            
    if(!_tcscmp(szReadedClass, pClassReq->m_szClassName))
            
    *ppOb = pClassReq->m_fnCreator();
        }
        
    else
        {
            
    *ppOb = ::gCreateObject(szReadedClass);
        }
        delete[] szReadedClass;
        
    if (*ppOb)
        {
            (
    *ppOb)->Read(*this);
            
    return S_OK;
        }
        
    return E_FAIL;
    }

    在此基础上,序列化中的另一个问题是,对象间的循环引用问题。在一个复杂的数据结构中,往往有A对象引用B对象,同时B对象又引用A对象。此时用上面的序列化机制,就会出现死循环。MFC以一种巧妙的方式解决了此问题。其核心思想是引入一个Hash表,将已经保存(或即将要保存)的对象指针与对象id(由系列化机制分配)联系起来。在第二次保存此对象时只是保存对象id,这样也就中断了循环。从这一个角度讲,序列化是相当优秀的。它可以轻易地保存任何一种复杂的数据结构。

    考虑了循环引用问题后的对象读写函数如下:
    void SArchive::WriteObject(SObject *pOb)
    {
        WriteString(pOb
    ->GetRuntimeClass()->m_szClassName);
        UINT nObIndex 
    = -1;
        
    if (x_pMap->Lookup(pOb, &nObIndex))
        {
            
    // 如果对象已经保存!
            *this << nObIndex;
        }
        
    else
        {
            
    *this << (UINT)-1;
            
    // 规定-1表示对象是正常存盘
            x_pMap->SetAt(pOb, ::gGetObIndex(pOb));
            
    // 这里gGetObIndex是系统为pOb对象分配id号的函数
            
    // MFC中只是简单地用一个递增编号而已。
            pOb->Write(*this);
        }
    }

    HRESULT SArchive::ReadObject(
        
    const SRuntimeClass *pClassReq,
        SObject 
    **ppOb)
    {
        
    *ppOb = NULL;
        LPCTSTR szReadedClass 
    = ReadString();
        UINT nObIndex 
    = -1;
        
    *this >> nObIndex;
        
    if (nObIndex != (UINT)-1)
        {
            
    // 已经被保存过的情形!!!
            if (pClassReq && 
                _tcscmp(szReadedClass, pClassReq
    ->m_szClassName))
            {
                delete[] szReadedClass;
                
    return E_FAIL;
            }
            delete[] szReadedClass;
            
    *ppOb = ::gGetObByIndex(nObIndex);
            
    return S_OK;
        }
        
    // 否则,正常情形,同以前一样!!!
        if (pClassReq)
        {
            
    if (!_tcscmp(szReadedClass, pClassReq->m_szClassName))
                
    *ppOb = pClassReq->m_fnCreator();
        }
        
    else
            
    *ppOb = ::gCreateObject(szReadedClass);
        delete[] szReadedClass;
        
    if (*ppOb)
        {
            (
    *ppOb)->Read(*this);
            
    return S_OK;
        }
        
    return E_FAIL;
    }



原创粉丝点击