软件开发>杂文>5

共享软件开发的点滴心得

[中文][English][日本語]

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

 Inverse inheritance

庙堂与江湖

刘新宇

2006年四月

 

北京在四月虽然是春天,却是干旱与多风。所以北方有“春雨贵如油”的民谚。然而却有很多的人川流不息于其中,而不知远千里之外江南,却是“和风细雨”,尤其是4月初,更是“清明时节雨纷纷”的景象。古代长期以来都是把北方的一些城市,如北京,长安,汴梁作为“庙堂”,而把江南一带称为“江湖”。

 

通常是居庙堂者,车马衣裘,趾高气扬;而谪江湖者,去国怀乡,满目萧然。千年来直至今日,无数公卿大夫、学者文人未能免 俗,连李白也一旦蒙天子征召入京就:“仰天大笑出门去”,更何况一般的凡人呢。然而却也有人,能够“居庙堂之高,则忧其民;处江湖之远,则忧其君”,例如北宋的范仲淹就是“先天下之忧而忧,后天下之乐而乐”。

 

范仲淹的“江湖与庙堂”观,是反其道而行之的。“反其道而行之能让人耳目一新,发现新的天地。长期以来,对于面向对象世界中的继承和多态,一直以来的“道”是子类继承父类,抽象衍生具体。如果“反其道而行之会是什么呢?如果“继承反转”,会不会天下大乱呢?

 

先看一个典型的问题及其传统解法:比如希望建立一套数字系统,不管整数,浮点数,统一纳入,那么传统的做法基本如此:

class Number{

public:

     virtual void display()=0;

     virtual ~Number(){}

};

class Integer: public Number{

public:

     Integer(int v):value(v){}

     void display(){ std::cout<<"an integer "<<value<<"\n"; }

protected:

     int value;

};

class Float: public Number{

public:

     Float(float v):value(v){}

     void display(){ std::cout<<"a float "<<value<<"\n"; }

protected:

     float value;

};

使用这套系统时,借助运行时多态和虚函数,大体如下:

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

     Number* num1 = new Integer(3);

     Number* num2 = new Float(3.14);

     num1->display();

     num2->display();

     delete num1;

     delete num2;

}

如果Number是抽象的最高层,也就是在政治中心“庙堂”,而各个具体的IntegerFloat则是在基层的“江湖”。现在“反其道而行之”,看看如何使用“继承反转”。

 

首先是“居庙堂之高,则忧其民”,Number愿意放下高高的架子,甘心从“子民”继承:

template<class ConcreteNumber>

class Number: public ConcreteNumber{

public:

     template<class T>

     Number(T v):ConcreteNumber(v){}

};

 

人在“江湖”的子民,不用做任何改动,“君为轻,民为重,社稷次之”,Number甘心在所有的ConcreteNumber下面“为人民服务”,调用方法如下:

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

     Number<Integer> num1(3);

     Number<Float>   num2(3.14);

     num1.display();

     num2.display();

}

 

输出结果一样。但是感觉似乎不对,这是“多态”么?虽然是*.display()形式的调用,可是num1num2分属不同的型别Number<Integer>Number<Float>,这和下面的代码有区别么?

Integer num1(3);

     Float   num2(3.14);

     num1.display();

     num2.display();

 

如何能够界定是否是“多态”?所谓“多态”(Polymorphism)是指具体事物对于抽象事物表现出各自独特的行为,使用者对于抽象事物的操作,最终结果依照其实际代表的具体事物而不同。

皇帝甚至不知道某个地方官的姓名,只知道一旨政令发出,大家会遵照执行,置于你是雷厉风行的执行,贪赃枉法的执行,还是再次交待给下级官员去执行则不去管了。

 

所以从居庙堂的“皇帝”角度看,多态就是对于抽象事物的一致使用,例如:

void printNumber(Number* n){

     n->display();

}

 

Number* num1 = new Integer(3);

Number* num2 = new Float((float)3.14);

printNumber(num1);

printNumber(num2);

 

同样,下面的用法也是对抽象事物的一致使用:

template<class NumberType>

void printNumber(NumberType n){

     n.display();

}

 

Number<Integer> num1(3);

Number<Float>   num2(3.14);

printNumber(num1);

printNumber(num2);

 

所以多态是语义上的概念,而实际使用中出现了运行时多态和编译时多态这两种不同的表现。

 

现在的问题是,“继承反转”有什么用,相比较传统做法,优点在哪里?甚至似乎根本不用声明一个Number<T>而直接可以把IntegerFloat传给printNumber<T>。继承反转不是画蛇添足么?这里仍然采用“提出问题——解决问题”的思路来回答这些疑问。比如现在需要让所有的数字类都支持copy构造函数和等号赋值,并且支持基本的逻辑大小运算。如果采用传统做法,需要的工作是:为Number接口增加copy ctor、等号,各个大于小于等于逻辑运算虚函数声明,然后依次给自上而下给IntegerFloatconcrete class实现上述接口,工作繁冗而重复。但是一旦有了“处江湖之远,则忧其君”的Number<T>,这些工作就可以自动化完成:

 

template<class ConcreteNumber>

class Number: public ConcreteNumber{

public:

     template<class T>

     Number(T v):ConcreteNumber(v){}

     template<class T>

     Number(const Number<T>& ref){ value = ref.value; }

     Number& operator=(const Number& ref) { value = ref.value; }

     bool operator==(const Number& ref) { return value == ref.value; }

     bool operator< (const Number& ref) { return value < ref.value; }

     bool operator> (const Number& ref) { return value > ref.value; }

};

由于Number反转继承自所有的ConcreteNumber,所以为Number书写的所有方法,也就自然添加给了Number<ConcreteNumber>,编译器会重复这些枯燥的重复工作,调用时如下:

Number<Integer> num1(3);

Number<Float>   num2(3.14);

Number<Integer> num3 = num1;

Number<Float>   num4(1.414);

if(num2>num4)

     num3.display();

如果能够把传统方法和反转继承结合起来,就会出现更加有意思的结果,也就是在原来Number-ConcreteNumber的基础上增加一个“忧民忧国”的范仲淹NumberBase,整个继承体系如下:

Number

|

|-----------------|-----------------|

Integer...            Float...         Short

|                 |                 |

NumberBase<Integer>  NumberBase<Float>  NumberBase<Short>

这样,可以在“庙堂”把Number定义为抽象接口,规定抽象的方法(protocal),然后在“江湖”由最底层的NumberBase<T>驱动编译器自动化生成大量重复的代码。并且还有一个额外的收获——“范仲淹”可以充当一个类型安全的工厂。

首先Number, Integer, Float的定义和实现和传统方法一致,然后定义“范仲淹”:

template<class ConcreteNumber>

class NumberBase: public ConcreteNumber{

public:

     template<class T>

     NumberBase(T v):ConcreteNumber(v){}

     template<class T>

     static NumberBase* Create(T v) { return new NumberBase(v); }

};

“范仲淹忧国忧民”,反转继承自所有的ConcreteNumber,因此也继承自Number。此后就可以利用“范仲淹”这个类型安全工厂来生产Number了:

std::vector<Number*> coll;

coll.push_back( NumberBase<Integer>::Create(3) );

coll.push_back( NumberBase<Float>::Create(3.14) );

coll.push_back( NumberBase<Float>::Create(1.414) );

for(std::vector<Number*>::iterator it=coll.begin();

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

     (*it)->display();

现在看看“范仲淹”(NumberBase)如何帮助“庙堂之君”(Number)驱动编译器为“江湖之民”(FloatInteger)生成赋值,逻辑运算符的代码:

首先“庙堂之君”规定要有如下接口(protocal):

class Number{

public:

     virtual Number& operator=(const Number& ref)=0;

     virtual bool operator==(const Number& ref)=0;

     virtual bool operator< (const Number& ref)=0;

     bool operator> (const Number& ref) {

          return !((*this)==ref || (*this)<ref);

     }

     virtual void display() = 0;

     virtual ~Number(){}

};

然后Integer, Float等什么都不做,而由“范仲淹”来驱动编译器自动生成:

template<class ConcreteNumber>

class NumberBase: public ConcreteNumber{

public:

     //...

     NumberBase& operator=(const Number& ref){

          if(const NumberBase* p=

dynamic_cast<const NumberBase*>(&ref)){

              value = p->value;

              return *this;

          }

          throw std::runtime_error("type mismatch");

     }

     bool operator==(const Number& ref){

          if(const NumberBase* p=

dynamic_cast<const NumberBase*>(&ref)){

              return value == p->value;

          }

          throw std::runtime_error("type mismatch");

     }

     bool operator< (const Number& ref) {

          if(const NumberBase* p=

dynamic_cast<const NumberBase*>(&ref)){

              return value < p->value;

          }

          throw std::runtime_error("type mismatch");

     }

};

此后,“政令一出,则四方咸应”:

std::vector<Number*> coll;

for(int i=0; i<10; ++i)

     coll.push_back(NumberBase<Integer>::Create(i));

for(int i=0; i<5; ++i)

     (*coll[i])=(*coll[i+5]);

for(int i=0; i<9; ++i)

     if((*coll[i])>(*coll[i+1])){

          std::cout<<"at "<<i<<" ";

          coll[i]->display();

     }

同样的方法,如果“庙堂之君”想加入算术运算加减乘除的话,不必担心要改动FloatInteger的一行,“江湖之民”的负担被减小到0,而工作由“先天下之忧而忧,后天下之乐而乐”的反转继承者完成。

现在仍然存在一个难题,假设“庙堂之君”希望加入settergetter,问题就出现了,Number中如何定义settergettervirtual function

class Number{

public:

     template<class T> virtual T get()=0;

     template<class T> virtual void set(T v) =0;

};

编译器会提示“member function templates cannot be virtual”,这样Number就必然退化为类模板Number<T>,而这不是这个继承体系最初需要的。但是,有没有统一一致的方法,可以对Number*或者Number&进行gettersetter呢?由于远在江湖的NumberBase<T>从所有的ConcreteNumber继承,所以可以把settergetter在其中实现,同时所有Number*或者Number&最终的具体对象一定是NumberBase<T>的对象,所以可以采用一个统一的Utility进行gettingsetting,具体实现如下。

首先作为辅助手段,每个ConcreteNumber要输出自己的内部数据类型信息。

class Integer: public Number{

public:

     typedef int   ValueType;

     //....

};

然后NumberBase<T>实现针对所有ConcreteNumbergettersetter

template<class ConcreteNumber>

class NumberBase: public ConcreteNumber{

public:

     //....

     template<class T>

     const T get(){ return value; }

     template<class T>

     void set(T v){ value=v; }

};

现在任何Number*或者Number&都可以down-castNumberBase<T>后调用这一对工具了。为了方便,可以把类似settergetter这类涉及内部型别的抽象方法,统一定义在一个NumberUtil内:

struct NumberUtil{

     template<class T>

     static const typename T::ValueType get(Number* ptr){

          if(NumberBase<T>* p = dynamic_cast<NumberBase<T>*>(ptr))

              return p->get<T::ValueType>();

          throw std::runtime_error("error type");

     }

     template<class T>

     static void set(Number* ptr, typename T::ValueType v){

          if(NumberBase<T>* p = dynamic_cast<NumberBase<T>*>(ptr))

              p->set(v);

          else

              throw std::runtime_error("error type");

     }

};

现在可以测试一下:

Number* num=NumberBase<Integer>::Create(0);

int raw=NumberUtil::get<Integer>(num);

std::cout<<raw<<”\n”;

raw++;

NumberUtil::set<Integer>(num, raw);

num->display();

 

目前为止,所有的继承关系仍然是NumberBase<T>单独继承自T,所以本质上天下不只一个“范仲淹”,而是一系列忧国忧民之士,他们都能够“处江湖之远,则忧其君”,各自负责各自的分内之事——辅助T,实际上如果NumberBase不是大量被Create出来,而是作为一个全局的Utility,比如叫NumberUtility,那么还有更加彻底的继承反转——NumberUtility同时多重继承所有的ConcreteNumber的辅助工具,以一肩担天下重任。看起来像这样:

template<class T>

class Utility{

     //...

};

template<class Integer, class Float, ..., class Fraction>

class NumberUtility:public Utility<Integer>,

public Utility<Float>,

...,

public Utility<Fraction>

{

     //..

};

这样,“庙堂之君”就可以将NumberUtility随心所欲的施加于他所有想操作的类型,编译器就会利用Utility的模板,为各个类型生成大量重复复杂的代码。

首先从简单情况开始,然后逐渐演化,现在要制作一个通用的Utility可以输出数字的文本格式,也可以根据未来的要求输出XML格式,同时在类型上支持int, float, double等类型,实现如下:

首先以普通文本格式输出数字的工具:

template<class T>

struct TxtUtility{

     void write(T v){ std::cout<<v<<"\n"; }

};

然后是以XML格式输出数字的工具:

template<class T>

struct XMLUtility{

     void write(T v){ std::cout<<"<value>"<<v<<"</value>\n"; }

};

现在定义一个通用数字输出工具,可以根据用户要求切换输出格式,支持多种数字类型:

template

     class T1, class T2, ..., class Tn,

     template<class> class Utility

struct NumberUtility: public Utility<T1>,

                     public Utility<T2>,

                     ...

                     public Utility<Tn>

{

     template<class T>

     void output(T v){

          Utility<T>* p=this;

          p->write(v);

     }

};

这个通用的NumberUtility使用方式如下

void test(){

     NumberUtility<int, short, ..., double, XMLUtility> util;

     util.output(0);

     util.output(3.14);

}

仔细思考上面的实现,似乎有些舍近求远,难道不能这样使用么?

XMLUtility util;

util.write(0);

util.write(3.14);

NumberUtility提供了一个中间层[2],实现了具体数字表达方式的实现和目标的分离。按照前面的表述,这是一个反转继承的抽象。有意思的地方在于,如果需要把Utility也纳入动态抽象,将write变成虚方法会出现什么情况?自始至终这个问题被一再推迟或者避开,因为编译器不允许定义含有模板的虚方法。对于这个问题的可行思路是,反转继承多个抽象类模板。也就是“庙堂”不再仅仅是一个,要求“范仲淹”以一身服务多个“庙堂”。

首先定义一组“庙堂”——抽象接口(类)模板:

template<class T>

struct AbstractUtility{

     virtual void write(T v)=0;

};

 

如果像前面定义NumberUtility那样同时多重继承每个“庙堂”,就会出现这样的树状类层次结构:

AbstractUtility<T1>  AbstractUtility<T2>...AbstractUtility<Tn>

|                 |                 |

|-----------------|-----------------|

|

NumberUtility<T1...Tn, AbstractUtility>

                              |___virtual void write(T1 v)=0;

                              |___virtual void write(T2 v)=0;

...

                              |___virtual void write(Tn v)=0;

 

NumberUtility这里类最有趣的特点是含有多个名字一样的纯虚函数。所以,按照动态多态的使用规则,任何NumberUtility的继承者,都必须overwrite所有这些虚函数,如果运用传统面向对象方法实现这些大量重复的函数,将会非常枯燥,大体如下:

 

struct XMLUtility: public NumberUtility<T1...Tn, AbstractUtilty>{

     void write(T1 v){

          std::cout<<"<value>"<<v<<"</value>\n";

     }

void write(T2 v){

          std::cout<<"<value>"<<v<<"</value>\n";

     }

     ...

void write(Tn v){

          std::cout<<"<value>"<<v<<"</value>\n";

     }

};

为了解决这个问题,Andrei[1]中给出了一个利用TypeList的方法。直接成果就是实作出了一个AbstractFactory,这里将对Andrei的方法做一些小小改动。将NumberUtility2层继承,变成多层次继承,如下:

AbstractUtility<Tn>         NumberUtility<...>

                              

----------------------------------

                            |

                            ...

                                

AbstractUtility<T2>         NumberUtility<...>

                               

----------------------------

              |

AbstractUtility<T1> NumberUtility<T2...Tn, Abst...>

                              

----------------------------

|

NumberUtility<T1...Tn, AbstractUtility>

也就是“范仲淹”这样的忧国忧民之士们,以类似二叉树的形式,形成一个整体。实现手段如下,首先需要引入AndreiTypeList定义:

struct NullType{};

template<class T, class U> struct TypeList{

     typedef T Current;

     typedef U Next;

};

TypeList本质像一个链表式的数据结构,可以以递归的方式,把一组型别串起来,例如:

typedef TypeList<int,

TypeList<double,

TypeList<float, NullType> > > NList;

有了上述工具,就可以构造上面图中的二叉树式反转继承的NumberUtility了,实现如下:

template<class TList, template<class> class Op> struct NumberUtility;

template<template<class> class Op> struct NumberUtility<NullType, Op>{};

template<class T, class U, template<class> class Op>

struct NumberUtility< TypeList<T, U>, Op>

     :public Op<T>, NumberUtility<U, Op>

{

     template<class Arg> void output(Arg v){

          Op<Arg>* p=this;

          p->write(v);

     }

};

 

首先这个NumberUtility接受一个TList参数,和一个抽象的Op作为参数,输出则是一个线性反转继承体系。实现原理是驱动编译器递归生成,递归的结束标志是遇到NullType。递归方法是,每次从TList中取出一个要处理的型别,交给Op写出一个特定的纯虚函数,然后把后继的型别子列表,递归交给自己继承。最后结果就是这个NumberUtility拥有了n个纯虚函数接口。

 

这里需要说明的一点就是output函数,NumberUtility里面有很多纯虚函数,为了告诉编译器要调用哪一个,output可以根据用户输入的参数型别推断出来,由于NumberUtility实际反转继承自所有的Op<T>,所以可以根据推断出的型别把自己up-casting成需要的类,然后调用那个类的虚函数。

 

下面,再用非常近似的手法,驱动编译器来实现(Implement)这n个虚函数。

首先定义一个以XML格式输出的实现模板,作为一族“江湖之民”:

 

template<class T, class Base>

struct XMLUtility: public Base{

     //implement the virtual functions

     void write(T v){ std::cout<<"<value>"<<v<<"</value>\n"; }

};

然后同样由一族“忧国忧民”之士们,依次将这个模板,套用到TList中的所有型别上:

template<

     class TList,

     template<class, class> class Helper,

     class Super

> struct ConcreteUtility;

template<template<class, class> class Helper, class Super>

struct ConcreteUtility< NullType, Helper, Super>: public Super{};

template<class T, class U, template<class, class> class Helper, class Super>

struct ConcreteUtility< TypeList<T, U>, Helper, Super>

     :public Helper<T, ConcreteUtility<U, Helper, Super> >{};

ConcreteNumberUtil接受一个型别列表,一个针对抽象模板类的实现模板,和前面生成的抽象NumberUtili接口。然后它驱动编译器递归为每个纯虚函数写一个实现,递归的结束条件为遍历到型别列表的结束标志NullType。这个继承体系和前面的二叉树型相反,是一个链状的体系:

NumberUtility(拥有n个虚函数)

ConcreteUtility<NullType...>

XMLUtility<Tn,...>(实现第n个虚函数)

ConcreteUtility<Tn,...>

...

XMLUtility<T2,...> (实现第2个虚函数)

ConcreteUtility<T2...Tn,...>

XMLUtility<T1,...> (实现第1个虚函数)

ConcreteUtility<T1,T2...Tn,...>

通过以上这些复杂的步骤,反转继承和传统继承进行结合,同时静态编译期多态和动态运行时多态也组装到一起。通过这样的“国事、家事、天下事,事事关心”的手段,完成了将天下庙堂与所有江湖全部纳入一体的功能,现在可以测试看一下效果了:

typedef NumberUtility<NList, AbstractUtility> BaseUtil;

typedef ConcreteNumberUtil<NList, XMLUtility, BaseUtil> DerivedUtil;

BaseUtil* util = new DerivedUtil();

util->output(3);

util->output(3.14);

delete util;

程序输出如下:

<value>3</value>

<value>3.14</value>

 

从表象上来看,似乎继承反转是传统继承的相反形式,然后其本质是从具体归纳抽象,再从抽象衍生具体。如果能够将静态多态和动态多态结合,最大限度发挥运行时和编译器的能力[3],就会取得意想不到的效果。但是也客观要求更加深入广泛的思考,真正达到“居庙堂之高,则忧其民;处江湖之远,则忧其君”,和“先天下之忧而忧,后天下之乐而乐”的境界。

 

参考书目:

[1] Andrei Alexandrescu, Modern C++ Design: Generic Programming and Design Pattern Applied. Addison Wesley, Feb. 2001

[2] Martin Fowler, Kent Beck, etc. Refactoring: Improving the Design of Existing Code. Addison Wesley, June, 1999

[3] Herb Sutter, Andrei Alexandrescue, C++ Coding Standards: 101 Rules, Guidelines, and Best Practice. Addison Wesley, Oct. 2004

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