软件开发>杂文>10

共享软件开发的点滴心得

[中文][English][日本語

[机器人制作] [软件开发] [豆知识] [Squeak] [关于]

 C++ Reflection

自省

刘新宇

20069

 

“君子博学而参省乎已,则知明而行无过矣。”

——荀子《劝学》

中国自古就有自省的传统,《论语》里有“吾日三省吾身━━为人谋而不忠乎?与朋友交而不信乎?传不习乎?”自省,就是自己检查自己。客观判断评价他人不难,难在有自知之明。

英文Reflectoin,翻译成中文的意思是:“反射,映像,倒映,反省,沉思,反映”。在软件世界,让Reflection很出名的是Java[1],随后的C#等等,都加入了对Reflection的支持。一般国内将其翻译为“反射”,初看时,顾名思义,不得其解。《Dive into python》对相关的内容使用了introspection一词。其中文译本《Python研究》中,译为“自省”。并给出了这样的解释:“自省是指代码可以查看内存中以对象形式存在的其它模块和函数,获取它们的信息,并对它们进行操作。[2]”。这一解释还是复杂并且抽象。Reflectoin的概念可以一直回溯到Smalltalk[3][4]

本文希望指出的是,“自省”的能力不是语言专有的,不应将其算作某种语言的特性。作为例子,本文给出一个在标准C++中,利用Type Object模式[5]实现Reflection的分析。

首先看看让C++程序员很羡慕的一段Java代码:

System.out.println("which Fruit you want to create?[Orange/Apple]");

DataInput in = new DataInputStream(System.in);

String name = in.readLine();

Fruit fruit = (Fruit) Class.forName(name).newInstance();

System.out.println(fruit.getName());

注意其中的黑体字部分。其作用是从一个字符串描述的类名称,创建这个类的一个实例。为了在C++中实现同样的功能,需要从Java语言的表象,抽出上述代码的本质。这里采用的方法仍然是从简单到复杂,一步一步构建出整个体系。为此,需要先分析一下,到底什么情况下,需要类似功能?有了这个功能会带来什么好处。

动物园问题

 

人类认知的过程是从简单到复杂,从具体到抽象[6]。 作为具体简单的例子,考虑一个动物园,动物园中豢养着各种动物,比如狮子,猴子等等。而狮子中,每头狮子都是不一样的。比如狮子爸爸,小狮子,狮子妈妈等 等,同样猴群中的每只猴子也各不相同,有猴王爸爸,猴子妈妈,小猴子弟弟,小猴子哥哥等。现在要为动物园开发一个动物管理系统,对于饲养员来说,每一只动 物都各不相同。因此很自然的,一个可行的思路是这样的:

... other animals ...

在这个设计中, 狮子和猴子被定义成类,狮子类中会有狮子爸爸,狮子妈妈,小狮子以及其他狮子,它们表现为狮子类中的实例(对象)。同样,猴子爸爸,猴子妈妈,小猴子弟 弟,小猴子哥哥等等都是猴子类的实例。这个设计看上去没有什么问题,即使猴群中新生出了小猴子妹妹也可以通过创建新的猴子对象得以处理。系统可以管理猴群 和狮群中的动物变化(放归野生等等)。

针对上面这个设 计,存在的一个潜在问题是,动物种类可能不是确定的。动物圆可能打算新引进熊猫和长颈鹿。面对这样的变化,这个动物管理系统需要做的是重新找来当初的开发 人员,编写出熊猫和长颈鹿的类,然后重新编译,调试,部署……。这对于一个迅速成长的动物园(每年甚至每月都要引进新品种动物)来说,可能是非常大的一个 麻烦。

造成这一麻烦的 原因就是,没有办法在不编译的情况下增加或者减少类。这一问题完全是运行时的问题,因此需要把注意力集中到运行时——运行时什么东西可以动态变化?这一问 题的唯一答案是类的实例(对象)。因此一旦希望代表动物的类可以动态变化,就只能把它们从静态的“类”便成动态的“实例(对象)”。为此需要引入一个更抽 象的类(例如“动物”),其实例是一种一种不同的动物(例如猴子,狮子)。而猴子和狮子中的每个个体,都需要知道自己属于哪种动物。这就形成了这样一种结 构:

针对这一基本结构,动物园的动物管理系统希望能够提供如下这些功能:

  • 能处理某一种动物(例如狮子)数目的变化。例如出生了新的小狮子,或者某头狮子被送到其他动物园。
  • 能处理动物种类数目的变化。例如动物园新引进了熊猫。
  • 能针对某种动物,进行某项操作。例如查询猴子的食谱,查询狮子的食谱。
  • 能针对某种动物中的个体,进行某项操作。例如查询新生狮子Simba是否注射过疫苗。

为此,以下分别实现这些功能,首先要创建AnimalObject类,所有的动物个体都是这个类的实例。按照上面的结构图,每个动物都有一个名字(比如饲养员起的名字),同时还有一个说明该动物属于哪种类的type。针对每个属性,都提供了一对方法用于设置和获得这些属性的内容。

class AnimalClass;

class AnimalObject{

public:

     AnimalObject(){}

     const AnimalClass* getClass() const { return itsClass_; }

     void setClass(const AnimalClass* aClass){ itsClass_=aClass; }

     const std::string toString() const { return name_; }

     void setName(const std::string& name){

          name_=name;

     }

private:

     const AnimalClass* itsClass_;

     std::string name_;

};

AnimalObject的定义没有任何奇特之处,下面要定义AnimalClass,并试图使得AnimalClass的实例(对象)能够模拟作为管理AnimalObject的类。首先需要给种动物一个名称,比如“Lion”或者“Monkey”,该名称被定义围AnimalClass的一个成员,并提供获取这个名称的方法getName(),这个名称可以由AnimalClass的构造函数初始化:

class AnimalClass{

public:

     AnimalClass(std::string name):className_(name){}

     AnimalClass& operator=(const AnimalClass& x){

          className_ = x.className_;

          return *this;

     };

     std::string getName() const { return className_; }

private:

     std::string className_;

};

由于AnimalClass要模拟成为AnimalObject的类,所以它必须提供一个产生AnimalObject实例的方法,按照Java的命名管理,这个方法可以叫做newInstance(),其实现如下:

class AnimalClass{

public:

     //....

     AnimalObject* newInstance(){

          AnimalObject* ptr= new AnimalObject;

          ptr->setClass(this);

          return ptr;

     }

     //....

private:

     std::string className_;

};

newInstance()中,首先会产生一个AnimalObject的实例(对象),然后通过setClass告诉该对象,他属于哪种动物。下面是一个说明该用法的测试:

AnimalClass Lion("Lion");

AnimalObject* kidLion = Lion.newInstance();

std::cout<<kidLion->getClass()->getName()<<"\n";

kidLion->setName("Simba");

std::cout<<kidLion->toString()<<"\n";

delete kidLion;

程序首先产生了狮子类,然后狮子类用newInstance()产生了小狮子这个实例(对象),小狮子可以用getClass()获得它所属于的动物类,其结果是“狮子类”,并且可以通过“狮子类”的getName()方法,打印出这种动物的名称。

管理所有动物

为了管理所有的动物,需要一个唯一的列表,用以维护目前动物园已有的动物种类,一个自然的想法是设计并实现一个类管理器,可以通过其增加和查询动物种类,如图:

 

参考Sigleton模式[7],可以很方便的写出这个管理类。考虑这个管理类的内部数据结构,由于希望能够用动物种类的名字,方便找到代表动物种类的对象,因此具有字典特性的Map最合适。

class ClassManager{

public:

     static ClassManager& instance(){

          static ClassManager inst;

          return inst;

     }

     //....

private:

     std::map<std::string, AnimalClass*> coll_;

};

其中instance提供了访问此Singleton的方法,而内部是一个按照动物种类的名字存储指针的字典。为了提供增加动物,按照名字检索动物的功能,以及最后释放内存的要求,还要增加一些成员函数和必要的析构,如下:

class ClassManager{

public:

     static ClassManager& instance(){

          static ClassManager inst;

          return inst;

     }

     void add(AnimalClass* x){

          coll_[x->getName()]=x;

     }

     AnimalClass& get(const std::string& name){

          return *coll_[name];

     }

     ~ClassManager(){

          std::map<std::string, AnimalClass*>::iterator pos;

          for(pos=coll_.begin(); pos!=coll_.end(); ++pos)

              delete pos->second;

     }

private:

     //略去隐藏构造函数等细节

     std::map<std::string, AnimalClass*> coll_;

};

有了动物类管理器,就可以实现一个类似JavaforName()方法,其作用就是输入一个动物种类的名称,返回代表这类动物的AnimalClass

//static

AnimalClass& AnimalClass::forName(const std::string& name){

     return ClassManager::instance().get(name);

}

如果想动物类询 问是否存在“狮子类”,动物类就会委托“类管理器”查找“狮子类”,并把结果返回,此后用户就可以利用这个结果创建实际的一头狮子。并且用户可以随时向类 管理器中追加其他动物种类,比如熊猫,然后在通过熊猫类创建实际的一只熊猫。下面的测试代码演示了向系统追加一个“熊猫类”,并创建一只叫“盼盼”的熊 猫。

void printInfo(AnimalObject* p){

     std::cout<<"object: "<<p->toString()

          <<" is an instance of class: "

          <<p->getClass()->getName()<<"\n";

}

int main(int argc, char* argv[]){

     ClassManager::instance().add(new AnimalClass("Panda"));

     AnimalObject* panpan = AnimalClass::forName("Panda").newInstance();

     panpan->setName("Pan-pan");

     printInfo(panpan);

     delete panpan;

}

这段程序的执行结果为:

object: Pan-pan is an instance of class: Panda

可定义的行为

 

现在的问题是,上面的这种实现,剥夺了为不同的动物设计各自行为的可能。比如原来可能这样设计:

class Monkey{

public:

     //...

     void climb();    //猴子可以爬树

};

class Lion{

public:

     //...

     void hunt();    //狮子可以狩猎

};

改用目前的设计后,由于狮子类,猴子类成了两个对象而不是C++中的类,同时每头狮子和每只猴子都是AnimalObject的实例(对象),所以无法实现针对狮子和猴子特定的行为。深入思考这个问题可以引出forName()在语义上的的潜在错误——如果用户定义了MonkeyLion,则AnimalClass::forName("Monkey")就会返回代表猴子类的对象,反之因为用户没有定义熊猫类,所以AnimalClass::forName("Panda")应该找不到定义,按照Java的规则,会发生异常。而这正是“动物园”这个题目中要求避免的。一种可行的解决方法是,新建立一个代表熊猫类的对象,负责管理所有的熊猫。然后这个熊猫类,负责创建熊猫个体以及操作所有的熊猫。

首先着手解决第一个问题:为不同种类的动物增加行为。这样用户就可以这样使用这个系统:

Lion* simba=new Lion("simba");

std::cout<<simba->getClass()->getName()<<"\n"//"Lion"

 

Lion* mufasa=dynamic_cast<Lion*>(

              AnimalClass::forName("Lion").newInstance());

mufasa->setName("Mufasa");

simba->hunt();

mufasa->hunt();

delete simba;

delete mufasa;

为了保证使用Lion自己的构造函数和使用AnimalClassnewInstance产生一致的结果,必须设法将Lion的构造函数信息保存到其对应动物种类实例(对象)中去。同样,针对猴子,熊猫等等,也必须将各自的构造函数信息保存起来。这里既可以用C++functor,也可以不用。为此首先需要把动物类的构造函数这个概念抽象出来:

 

下面是这个构造函数这个概念的抽象:

class Constructor{

public:

     virtual AnimalObject* newInstance()=0;

};

狮子类可以将其实现为:

class LionConstructor: public Constructor{

public:

     Lion* newInstance(){

          return new Lion;

     }

};

并且,还需要在AnimalClass中记录其所代表的动物的构造函数,并修改原先的newInstance,如下:

class AnimalClass{

public:

     //....

 

     ~AnimalClass(){ delete ctor_; }

     AnimalObject* newInstance(){

          AnimalObject* ptr = ctor_->newInstance();

          ptr->setClass(this);

          return ptr;

     }

     const Constructor* getConstructor() const { return ctor_; }

     void setConstructor(Constructor* ctor){ ctor_ = ctor; }

private:

     Constructor* ctor_;

     //....

};

这样就可以定义狮子类自己的行为了:

class Lion: public AnimalObject{

public:

     Lion(){}

     void hunt(){ std::cout<<"hunting...\n"; }

};

注意狮子类需要从AnimalObject继承,因为AnimalClass不关心具体的动物种类,而是针对抽象的AnimalObject操作(比如newInstance)。最后,在使用狮子类前,还需要将狮子类登记到动物种类管理器ClassManager中去。方法如下:

AnimalClass* lionClass=new AnimalClass("Lion");

lionClass->setConstructor(new LionConstructor);

ClassManager::instance().add(lionClass);

此后,如果运行最开始那段测试代码,程序就会输出:

Lion

Hunting...

Hunting...

解决完第一个问题后,在着手解决第二个问题,如何动态的生成熊猫对象?由于熊猫是新增加的动物种类,以前并未登记,所以一旦用forName查找,就会发现查不到,此时程序应该创建一个AnimalClass的实例,命名为熊猫,并增加到ClassManager中去,但是由于没有为熊猫定义Constructor,所以熊猫类的ctor_为空指针。这样一旦运行熊猫类的newInstance,其中中的ctor_->newInstance()就会发生问题,解决方法是做一个判断:

AnimalObject* AnimalClass::newInstance(){

     AnimalObject* ptr;

     if(ctor_)

          ptr = ctor_->newInstance();

     else

          ptr= new AnimalObject;

     ptr->setClass(this);

     return ptr;

}

这样,对于用户明确定义过的狮子类和猴子类,就会使用多态机制调用各自的构造函数,而对于后来动态加载的熊猫类,就会退一步创建一个AnimalObject对象来代表一只熊猫个体。具体的实现留给读者作为练习。

针对行为的自省

到目前为止,利用已有的设计和实现,已经可以能够针对一个AnimalObject的对象,得知其所属的动物种类。并且还可以动态的根据动物种类的名字,创建不同的动物个体。并且这样创建出来的对象和直接利用new创建出的对象一致。如果把目前代码中的Animal这个字符串去掉,就得到了一个——通用的自省系统框架。但是目前尚不能根据一个对象,通过自省了解到它有哪些行为,并且利用这些行为的名字调用对应的方法,从而操作这个对象。实现这个功能后,用户就可以写出如下的代码:

void function(Object* obj){

     std::cout<<"obj has the following methods:\n";

     typedef std::map<std::string, Method*> Methods;

     typename Methods::iterator it;

     Methods coll=obj->getClass()->getMethods();

     for(it=coll.begin(); it!=coll.end(); ++it)

          std::cout<<it->first<<"\n";

    

     Method* func=obj->getClass()->getMethod("hunt");

     func->invoke();

}

仿照前面针对ctor的设计,这部分也可以用类似的方法实现。需要定义个抽象的Method,并提供invoke方法来执行指定的行为。然后在Class中利用一个字典记录所有可以被自省的methods。由于其实现和前面极为类似,故这里略去,读者可以作为练习自行实现。

利用C++特性进行改进

现有的实现在使用中,尚有很多不方便的地方。观察狮子和猴子的构造函数类:

class LionConstructor: public Constructor{

public:

     Lion* newInstance(){

          return new Lion;

     }

};

class MonkeyConstructor: public Constructor{

public:

     Monkey* newInstance(){

          return new Monkey;

     }

};

它们极为相似,但是用户却要亲自重复地写出类似的代码为每种动物写格子的构造函数类。我们可以利用C++模板的功能自动生成代码(参考《庙堂与江湖》),如下:

template<class T>

struct Ctor: public Constructor{

     T* newInstance(){ return new T; }

};

这样用户在注册类时,就可以这样写:

Class* lionClass=new Class("Lion");

lionClass->setConstructor(new Ctor<Lion>);

ClassManager::instance().add(lionClass);

用户向类管理器注册类这一步骤,尚不够简便,用户需要手工在程序的某个地方,注册其定义的类,一旦遗忘,就会造成潜在的问题。为此可以利用C++的宏,定义一个DELCARE_REFLECTIVE(x)和一个IMPLEMENT_REFLECTIVE(x)在定义狮子时,在类里面声明DELCARE_REFLECTIVE,然后在狮子类的实现模块(cpp)文件中,声明IMPLEMENT_REFLECTIVE,这两个宏就自动创建好狮子的类对象,并将其加入到类管理器中,这两个宏声明如下:

#define DECLARE_REFLECTIVE(x)     \

static bool registerClass(){ \

     Class* inst = new Class(#x); \

     inst->setConstructor(new Ctor<x>);    \

     ClassManager::instance().add(inst);   \

     return true;  \

}

#define IMPLEMENT_REFLECTIVE(x)   \

namespace{    \

     const bool reflect_res=x::registerClass(); \

}

使用时,在Lion.h中:

class Lion: public Object{

public:

     //....

     DECLARE_REFLECTIVE(Lion)

};

Lion.cpp的开头加入

IMPLEMENT_REFLECTIVE(Lion)

结论

 

本文给出了使用Type Object模式实现一个简单的反射系统的例子。由于篇幅有限,并没有实现所有的细节。但是通过这个实例,已经可以说明,系统自省的能力,不是语言相关的能力,而是脱离语言之外的。完全可以通过提供一个Reflection框架,或者说一个类库,而实现该功能。该框架中的单根继承——也就说所有的类从Object继承——的特点非常瞩目。另外通过使用C++的自身特点,例如《庙堂与江湖》中的范型自动化工厂,还能够进一步简化这个框架。

参考书目:

[1] Sun Microsystems, Inc. Java Core Reflection. http://java.sun.com/j2se/1.3/docs/guide/reflection/spec/java-reflectionTOC.doc.html

[2] Mark Pilgrim. Dive into python. Apress 2004 July 19

[3] Wikipedia Smalltalk. http://en.wikipedia.org/wiki/Smalltalk_programming_language

[4] Fred Rivard. Smalltalk: a Reflective Language. http://www2.parc.com/csl/groups/sda/projects/reflection96/docs/rivard/rivard.html

[5] Ralph Johnson and Bobby Woolf. The Type Object Pattern. http://www.ksc.com/article3.htm

[6] Mark Guzdial. Squeak: Object Oriented Design with Multimedia Applications. Prentice Hall, Dec. 20th, 2000.

[7] Erich Gamma, Richard Helm, Ralph Johnson, John Vlissides. Design Patterns: Elements of Reusable Object-Oriented Software. Addison-Wesley Professional Jan. 15. 1995


[English][上一篇][下一篇][目录][主页][联系我们: liuxinyu95@gmail.com]

ċ
Xinyu LIU,
Apr 9, 2012, 1:52 AM
ċ
Xinyu LIU,
Apr 9, 2012, 1:52 AM
Comments