关闭

C++之歌——求泛型给我安慰

标签: c++算法oopfuniteratorclass
2160人阅读 评论(2) 收藏 举报

编程是艺术,这无可否认。不信的去看看高大爷的书就明白了。艺术对于我们这些成天挤压脑浆的程序员而言,是一味滋补的良药。所以,在这个系列中,每一篇我打算以艺术的形式开头。啊?什么形式?当然是最综合的艺术形式。好吧好吧,就是歌剧。当然,我没办法在一篇技术文章的开头演出一整部歌剧,所以决定用一段咏叹调来作为开始。而且,还会尽量使咏叹调同文章有那么一点关联,不管这关联是不是牵强。

 

求泛型给我安慰

“求爱神快给我安慰,

别让我再悲伤流泪!

让我丈夫回转身旁,

或者让我死亡!”

这是W. A. Mozart著名歌剧《费加罗的婚礼》第二幕开头,伯爵夫人罗西娜的一段摇唱。太美了,就是前奏长了点。《费加罗的婚礼》取材于法国戏剧家博马舍创作的“费加罗三部曲”的第二部。伯爵阿尔马维瓦在《塞维利亚理发师》(三部曲第一部,由罗西尼创作成歌剧,晚于《费加罗的婚礼》)中,苦苦追求罗西娜,在费加罗的帮助下,终成眷属。但阿尔马维瓦终于显露出他的喜新厌旧、朝三暮四的本性。成为伯爵夫人的罗西娜受到冷落,郁郁寡欢。在自己的卧房中,忧伤地唱起了这首咏叹调

 

我们知道,程序员的朝三暮四,虽说不一定都是本性,但也几乎成了一种习惯。每当出现一种新的语言,很多程序员也不顾一切地投怀送抱。不管这种语言是好是坏,进步还是退步。尽管我们无法指责这种行为,就像无法把阿尔马维瓦伯爵告上法庭。但是,事实的真相还是应当昭示与天下的。

牢骚就不再多发了。今天的主题是GP。不,不是电池,也不是摩托比赛。全称是Generic Programming,泛型编程。一种非常强大,却有为人们所忽略的关键性技术。这种技术代表的是未来。

不,不要提JavaC#。他们的那种也叫Generic(泛型)的东西,和真正的GP沾不上什么边。只能算作大号的OOP,一会儿就会知道为什么我这么说。

本文起源于TopLanguagehttp://groups.google.com/group/pongba)上的一次讨论(http://groups.google.com/group/pongba/browse_thread/thread/e553a21476ba2ebd)。我在讨论中做了一个案例,以说明GP的作用。现在我把这个案例整理出来,一同探讨。这个讨论还涉及了更深层次的理论和技术,其余内容看那个帖子。

案例提出这样一个需求:

搞过帐务系统或者学过财务的都应该知道,帐务系统里最核心的是科目。在中国,科目是分级的,(外国人好像没有法定的分级体系),一般有45级,多的有78级,甚至10多级。不管怎么分,科目的结构是树。

在科目上有一个操作称为"汇总",就是把子科目的金额累加起来,作为本科目的金额。这实际上是对指定科目的所有下级科目的遍历汇总。这是一个非常简单,但却非常重要的帐务操作。

首先,我通过两种方法:OOP和传统的SP来实现这种操作。先来看SP

//科目类,具备树形结构

class Account

{

public:

   typedef vector<Account> child_vect;

pubilc:

   child_vect children();  //子级科目

   int account_type();     //本科目类型

   float ammount();        //本科目的凭证金额累计,非最明细科目返回0

};

//汇总算法

double collect(Account& item) {

 double result(0);

   Account::child_vect& children=item.children();

   if(children.size==0)        //最明细科目,没有子科目

       return  ammount(item.ammount);

   Account::child_vect::iterator ib(children.begin()), ie(children.end());

   for(; ib!=ie; ++ib)

   {

       result+=collect(*ib, filter, ammount);

   }

   return result;

}

当我需要对一个科目对象acc_x执行汇总算法,那么就是这样:

double res=collect(acc_x);

这非常简单。不过请注意,我这里还是利用了OOP的封装机制,为了使Account的实现和接口分离。但所使用的算法/数据分离的模式,则是SP风格的。

OOP风格的更加简单:

class Account

{

public:

   child_vect children();  //子级科目

   int account_type();         //本科目类型

   double ammount() {           //此成员直接执行子级科目的汇总任务

       double result(0);

       if(children.size==0)    //最明细科目,没有子科目

           return  m_ammount;  //或从其他途径获得,如数据库访问

       T::child_vect& children=item.children();

       T::child_vect::iterator ib(children.begin()), ie(children.end());

       for(; ib!=ie; ++ib)

       {

           result+=ib->ammount();

   }

   return result;

   }

};

使用起来是这样:

double res=acc_x.ammount();

都很好。不过,现在项目增加了需求,我们面临挑战:

假设现实世界的古怪客户,使我们面临一个挑战:他们的业务模型中,有部分科目不参与汇总计算,是一群特殊的科目。(这种科目我还真见过)。

那么,SP方式,可以另写一个collect函数:

double collect(Account& item) {

   //假设g_SpecialAccounts是个Singleton,负责管理特殊科目

   if(g_SpecialAccounts.IsSpecial(item->account_type()))

           return 0

   … //其余代码与原来的collect相同

}

OOP方式,则需要修改Account类(也可以利用重载和多态):

class Account

{

public:

  double ammount() {

   //假设g_SpecialAccounts是个Singleton,负责管理特殊科目

   if(g_SpecialAccounts.IsSpecial(account_type())

       return 0;

  }

   … //其余代码与原来的Account相同

};

相比之下,SP更灵活些。如果我没有Account的源码,或者我无法修改Account,那么我可以直接重写一个collect(比如,collect_x)也能解决问题。而在这种情况下,OOP方式,只能重载或重写这个类。(重载不仅仅需要重写相关成员,而且还需要编写诸如构造函数等辅助代码)。更深层次的因素,是代码耦合的问题。关于这个问题请看前面给出的那个讨论。

接下来的一个需求,则提出了更大的挑战:

我们如果注意的话,MIS系统中有很多地方同科目有着相同的逻辑结构。比如,销售部门的分销组织机构,一个企业的部门组织机构。在这些结构上,通常也会发生汇总操作,比如某个省的分销商业绩汇总,或者某个部门的人数汇总。

于是,充满优化意识的程序员,会想到复用在帐务系统上已有的成果。假设我们定义了部门类:

class Department

{

public:

   typedef vector<Account> child_vect;

public:

   child_vect Children();

   int dept_type();

   int employee_num();

...

};

对于SP方案而言,意味着需要写一个collect算法,可以同时用于这两个(甚至更多)的类型。我们努力地尝试着:

double collect_g(void* item, bool (*pred)(void*), void (*mem)(void*, void *)) {

   if(pred(item))

       return 0;

   double result(0);

   vector<void*>& children=item.children();

   if(children.size==0)        //最明细科目,没有子科目

   {

       mem(item, &result);

       return  result;

   }

   vector<void*>::iterator ib(children.begin()), ie(children.end());

   for(; ib!=ie; ++ib)

   {

       result+=collect(*ib, pred, mem);

   }

   return result;

}

此外,需要为AccountDepartment分别编写两个辅助函数:

//Account

bool Account_Pred(void* item) {

   Acount* acc_=(Account*)item;

   return  g_SpecialAccount.IsSpecial(acc_);

}

void Account_Ammount(void* item, void* val) {

   Acount* acc_=(Account*)item;

   *((double*)val)=acc_->ammount();

}

//Department

bool Dept_Pred(void* item) {

   Department* dpt_=(Department*)item;

   return  g_SpecialDepartment.IsSpecial(dpt_);

}

void Dept_EmpNum(void* item, void* val) {

   Department* dpt_=(Department*)item;

   *((int*)val)=dpt_->Employee_Num();

}

用起来,则是这样:

double res1=collect(&acc_x, &Account_Pred, &Account_Ammount);

int res2=collect(&acc_x, &Dept_Pred, &Dept_EmpNum);

对于OOP方案而言,则没有那么简单了。我们尝试着一步步来:

首先,应设法将算法分离。按照标准的OOP思路,用一个接口封装算法:

class ICollect

{

public:

   virtual ??? cacul(??? item)=0;

};

class AccountCollect : publie ICollect

{

public:

   AccountCollect

   virtual ??? cacul(??? item){…};

};

class DepartmentCollect : publie ICollect

{

public:

   virtual ??? cacul(??? item){…};

};

但是,我们立刻发现了问题,这些???的地方填什么?由于Account上需要汇总double类型的ammount,而Department上需要汇总人数,是int类型。两种实现的cacul()的类型不同。为解决问题,使用variant。参数item对应AccountDepartment,是不同的类型。解决的办法,就是使AccountDepartment(以及其他想共享算法的类型)都实现同一个接口:

class Account : public IData{…};

class Department : public IData {…};

于是,ICollect的接口变成:

class ICollect

{

public:

   virtual variant cacul(IData* item)=0;

};

当然,还有一些细节上的问题还没解决,比如,IData中定义那些成员,如何处理类型问题等等。这里限于篇幅,不再深究了。

到目前为止,两种方案中,SP明显比OOP来的简洁灵活。原因还是因为耦合。OOP为了解决不同类型的共享算法问题,不得不通过接口一层一层地将类型归一化。其结果就是形成肥厚的“粘合层”(详见那个讨论)。SP天然地将算法和数据分离,并且通过强制类型转换将类型归一化。(尽管OOP也可以通过强制类型转换归一化类型,但受制于类的集成结构,也无法象SP那样灵活)。但是,强制类型转换所带来的负面作用,是人所共知的。

两种方案,都有共同的一个特点,就是类型归一化。SP方案的collect算法必须将类型归一化为void*,并在计算时转换回来。而OOP方案的ICollectIData接口也都是为了归一化不同的类型而“强行”添加的。

针对这个问题,我们可以做一个想象(不是白日梦):如果算法不是只接受一种类型,而是可以接受一族类型的实例,而这些类型具备某些共有的特征。那么我们就无需象SP那样强制类型转换,或象OOP那样凭空增加一堆接口。

这就是GP的工作。

下面,我利用GP来改进SPOOP的实现。首先是SP

在改进版的SP实现中,AccountDepartment都没有什么变化,变动的是collect算法:

template<typename T, typename Pred, typename Sub, typename M>
typename M::return_type collect(T& item, Pred p, Sub s, M m) {
   if(p(item)==false)
       return  0;

   typename M::return_type result(0);

   T::child_vect& children=s(item);
   if(children.size()==0)
       return m(item);

   T::child_vect::iterator ib(children.begin()), ie(children.end());
   for(; ib!=ie; ++ib)
   {
       result+=collect(*ib, p, s, m);
   }
    return result;
}

(需要感谢一下pongba,他指出了原来算法的一个不足,我按照他的思路改进了算法,进一步减小了算法的耦合度,提高了灵活性)。

先解释一下模板参数:T,被计算的对象的类型,AccountDeparment等等;Pred,判别项目是否参与计算的“可调用物”的类型,可以认为是delegateSub,是用于从被计算对象上提取子级对象的“可调用物”类型;M,用于从被计算对象上获取计算数据的“可调用物”类型。

现在看一下如何使用这个算法:

double res1=collect(acc_x, mem_fun(&SpecialAccount::IsSpecial),

       mem_fun(&Account::children), mem_fun(&Account::ammount));

int res2=collect(dpt_x, mem_fun(&SpecialDept::IsSpecial),

       mem_fun(&Department::children), mem_fun(&Department::empl_num));

调用很长,比较晦涩,简单地解释一下:前一个调用把一个科目节点作为第一参数;第二个参数用mem_fun把特殊科目管理器绑定成一个“可调用物”(关于mem_fun参考stl);第三个参数包装了获取子级科目的成员函数;第四个参数包装了取得科目金额的成员函数。后一个调用也一样,只是把Account,及其相关内容,换成Department

引入GP后,算法collect变成了一个计算框架。具体的对象判别,子级获取,数据获取,都参数化。使得这个算法是“可装配的”,根据不同的业务类型,和被计算的成员函数和数据类型,由算法的使用者在使用时“拼装”起来。

相比没有使用GPSP方案,这个方案是类型安全的,无需使用强制类型转换。同时,原有的灵活性非但没有降低,反而提高。因为算法的三个delegate不再针对一个函数签名的,而是针对一簇函数签名,无需再为成员函数的返回类型变化,而放弃强类型的保护。更进一步,GP提高了整个算法的复用性,不再需要为每一个delegate包装器编写代码,只需复用已存在的mem_fun即可。

对于OOP而言,GP可以为其带来更好的效果:

//Biz_Alg类包含了若干常用的算法。做成traits形式,便于未来针对特别情况特化。template<typename T>
struct Biz_Alg
{
   template<typename R, typename C, typename Pred, typename Sub, typename M>
   R collect(C& item, M mem) {
       //
执行遍历汇总,调用item子节点的mem成员
   }

};

 //Account类和Department

template<typename Alg=Biz_Alg<Account> >

class Account

{

public:

   float ammount() {

       Alg::collect(*this, mem_fun(&SpecialAccount::IsSpecial),

               mem_fun(&Account::children), mem_fun(&Account::ammount));

   }

  

};

template<typename Alg= Biz_Alg<Department> >

class Department

{

public:

   float elec_fee() {

       return Alg::collect(*this, &Department::elec_fee);

   }

   int empl_num() {

       Alg::collect(*this, mem_fun(&SpecialDepartment::IsSpecial),

           mem_fun(&Department::children), mem_fun(&Department::ammount));

   }

   …

};

使用起来倒是比GP化的SP方案更简单:

float res1=acc_x.ammount();

int res2=dept_x.empl_num();

这种方案则是以OOP为出发点,利用GP技术使其解耦,并组件化。而前一个方案则是以SP为出发点,利用GP使其泛化和组件化。也就是说,GP被分成了两种风格:OO风格的GPSP风格的GP。从本质上来说,两者是等价的,都是将算法和数据对象分离。只不过前者是以类为主导,而后者则是以自由函数为主导。两者各有优势:前者集成度高,使用起来简便、清晰,更符合业务逻辑;后者更灵活,耦合度更小,扩展性更强。

根本上而言,OO风格的GP是以业务逻辑为核心,用类封装具体实现,程序员的注意力焦点都在业务逻辑上。比较适合高层的业务级别开发。而SP风格的GP ,以数据操作为核心,由自由函数将算法施加在数据实体上,程序员的注意力都集中在算法和数据本身上。这种风格比较适合底层的系统级开发。

从原先纯OO的方案来看,由于受制于强类型系统的约束,无法很优雅地将算法与数据分离。由此造成越来越严重的耦合。而GP 的引入,则利用其泛型特性,消除了强类型的负面作用,实现了解耦。

同样,在纯SP 方案中,算法与数据分离,但却没有完全独立于数据。一个自由函数要么只能针对一种类型(除非放弃类型安全的保证,和代码复用),要么促使数据对象的类型归一化。后者将依旧会引发数据对象间的耦合。但GP引入后,算法不再针对单一的类型,而是可以面向一族类型。在这种情况下,类型安全得到保证,代码复用得到实现,也无需强制地将不相关的类型归一化。

至于JavaC#的泛型存在的问题就很明显了。我们利用GP费尽心机,使得AccountDepartment不再需要实现同一个接口,大大弱化了类间的耦合。但JavaC#的泛型都要求类型参数共同继承自一个基类或接口。这反而迫使我们让类型耦合,做原本力图消除的事。这就是为什么JavaC#的泛型没什么用处的原因。

最后,我这里还有一个方案:

template<typename C, typename R, typename PredR (C::*mem)() >

class collect

{

public:

   collect(C const& item): m_Item(item), m_Mem(mem){}

   R operator() {

       //执行遍历汇总

   }

private:

   C&      m_Item;

   Pred    m_Pred;

   R (C::*m_Mem);

}

//用起来像这样

typedef collect<Account, float, AllTrue<Account>,

               &Account::ammount() > AccountCollect;

typedef collect<Account, int, AllTrue<Department>,

               &Department::employee_num > DeptElecFeeCollet;

//使用SP风格的GP方案中的AccountDepartment的对象,即简单类

float res=AccountCollect(acc1)();

float res=AccountCollect(dept1)();

这种方式看上去有些古怪。实际上使用一个函数对象封装了算法,构成了一个"算法适配器"collect是个算法框架,通过typedef 将相应的判别式、成员函数,同框架一起组装起来,构成一个类型。之后,直接使用这个类型,适配一个对象,以执行相应的计算。这可以看作是介于OOPSP 风格之间的一种GP方案。它比OOP风格方案稍微灵活一点点,比SP 风格方案使用上稍微简便一点点。但是,在这个方案能为我们带来多少好处,我不敢说。放在这里,只是为了展示GP的几种可能的设计方案而已。

总结一下。在这里。我们首先试图用OOPSP两种设计风格实现一个比较复杂的任务:让两种完全不同的业务类,共享相同的算法,而且希望获得最大的扩展性。但是,两种方案都不尽如人意。随后,我们引入GP,非常好的解决了两种方案的问题,却没有牺牲各自的优点。从中,我们可以看出,无论是OO还是SP,都可以通过GP来加以扩展,消除各自的缺陷。也就是“用OOSP设计,用GP来优化(代码)”。

 

阿尔马维瓦实际上另有新欢,垂涎于费加罗的未婚妻——苏珊娜。然而,足智多谋的费加罗安排下了计谋。伯爵接到苏珊娜的信,约其夜晚在花园幽会。他大喜过望,欣然赴约。正当他大献殷勤的时候,四周灯火通明,伯爵发现怀里抱着自己的妻子罗西娜。伯爵羞愧难当,只好当众下跪向罗西娜道歉,保证以后再也不犯。聪明的费加罗大获全胜,顺利的与苏珊娜举行了婚礼。

0
0

查看评论
* 以上用户言论只代表其个人观点,不代表CSDN网站的观点或立场
    个人资料
    • 访问:186878次
    • 积分:2431
    • 等级:
    • 排名:第15846名
    • 原创:44篇
    • 转载:0篇
    • 译文:0篇
    • 评论:110条
    最新评论