C++之歌——求泛型给我安慰
编程是艺术,这无可否认。不信的去看看高大爷的书就明白了。艺术对于我们这些成天挤压脑浆的程序员而言,是一味滋补的良药。所以,在这个系列中,每一篇我打算以艺术的形式开头。啊?什么形式?当然是最综合的艺术形式。好吧好吧,就是歌剧。当然,我没办法在一篇技术文章的开头演出一整部歌剧,所以决定用一段咏叹调来作为开始。而且,还会尽量使咏叹调同文章有那么一点关联,不管这关联是不是牵强。
求泛型给我安慰
“求爱神快给我安慰,
别让我再悲伤流泪!
让我丈夫回转身旁,
或者让我死亡!”
这是W. A. Mozart著名歌剧《费加罗的婚礼》第二幕开头,伯爵夫人罗西娜的一段摇唱。太美了,就是前奏长了点。《费加罗的婚礼》取材于法国戏剧家博马舍创作的“费加罗三部曲”的第二部。伯爵阿尔马维瓦在《塞维利亚理发师》(三部曲第一部,由罗西尼创作成歌剧,晚于《费加罗的婚礼》)中,苦苦追求罗西娜,在费加罗的帮助下,终成眷属。但阿尔马维瓦终于显露出他的喜新厌旧、朝三暮四的本性。成为伯爵夫人的罗西娜受到冷落,郁郁寡欢。在自己的卧房中,忧伤地唱起了这首咏叹调…
我们知道,程序员的朝三暮四,虽说不一定都是本性,但也几乎成了一种习惯。每当出现一种新的语言,很多程序员也不顾一切地投怀送抱。不管这种语言是好是坏,进步还是退步。尽管我们无法指责这种行为,就像无法把阿尔马维瓦伯爵告上法庭。但是,事实的真相还是应当昭示与天下的。
牢骚就不再多发了。今天的主题是GP。不,不是电池,也不是摩托比赛。全称是Generic Programming,泛型编程。一种非常强大,却有为人们所忽略的关键性技术。这种技术代表的是未来。
不,不要提Java、C#。他们的那种也叫Generic(泛型)的东西,和真正的GP沾不上什么边。只能算作大号的OOP,一会儿就会知道为什么我这么说。
本文起源于TopLanguage(http://groups.google.com/group/pongba)上的一次讨论(http://groups.google.com/group/pongba/browse_thread/thread/e553a21476ba2ebd)。我在讨论中做了一个案例,以说明GP的作用。现在我把这个案例整理出来,一同探讨。这个讨论还涉及了更深层次的理论和技术,其余内容看那个帖子。
案例提出这样一个需求:
搞过帐务系统或者学过财务的都应该知道,帐务系统里最核心的是科目。在中国,科目是分级的,(外国人好像没有法定的分级体系),一般有4、5级,多的有7、8级,甚至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;
}
此外,需要为Account和Department分别编写两个辅助函数:
//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);