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

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;
    }

你可能感兴趣的:(应用程序框架设计(3):RuntimeClass与序列化)