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;
returng_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;
returng_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);
[解决办法]
迅速mark
[解决办法]
我已经MARK!
本主席最崇拜LZ了!
MARK,Reading~
[解决办法]
呵呵,去TopLangue组,看看那个讨论,以及另一个后续讨论,我为什么会搞出这个案例。
我一直在奇怪,为什么总有人在烦恼类耦合问题。我以前没有这种概念,遇见了也不知道。现在一直使用GP,很少有耦合问题,即使有了,也很容易解决。通过这次讨论,我明白了,耦合问题都是滥用OOP的结果。
-----------------------------------------------
有点疑问,是滥用OOP的结果,还是用OOP不到家的结果?如果是滥用OOP的结果,那说明这是OOP
自身的缺陷,而要是用OOP不到家,那就是人的问题了
根据哲学里面矛盾的对立与统一观点,事物总是有利就有弊,我想OOP也不例外,虽然现在对OOP
的呼声很高,但也不可否认它一定也有自身的弱点,我不知道楼主上面所说的是不是就是OOP的
一个弱点。
从楼主的文章中,确实能看到楼主深厚的功力,真的从心底里敬佩!
[解决办法]
正确使用OOP会形成低耦合的代码(想想《设计模式》吧);所以如果代码是高耦合的,那么一定是用OOP不到家。
另外从词义的角度,我觉得用OOP不到家也是一种“滥用”,当然不适合使用OOP的地方就更不能滥用了,呵呵。
[解决办法]
在这里面,我很佩服lz的为cpp卫道的精神。
大致的读了以下这篇文章,没有太多的耐心读完,因为玩oo,没有uml的描述,
看起来很累人。嗯,那些uml的描述元素,估计我现在记得的也不多了,
什么时候忘光,什么时候就真的和it无缘了。
关于设计,这类的问题常常很复杂,评价一个好的设计,真的是没有一个统一的标准。
以前我也做过设计,类似的问题也解决过,这里也乱弹一下我的意见吧。
我很反对已开始就进入代码,而忽略了分析,
所以,我看了半天代码才知道你要表达什么意思。
一个设计的生命力在于它有多高的抽象程度,
抽象程度越高,那么,适应的范围就越广,在实际的运用中,
就越不容易被改变,从而也就成功地保持了自我,甚至成为成功的模式。
在这里,
我们的目的:对科目进行汇总。仅此简单的操作而已。当然还有保持高扩展性。
已有的约束(环境):科目类有很多属性。科目的结构是树状的。
更远一点,可能还要把部门也作为统计的界定范围。
ok,现在开始yy,在这里,要搞定清楚我们所做的汇总,这些汇总是条件的,
所谓的条件的真正含义是什么呢?实际上是是一种划分,不管条件多么复杂,
到最后,目标集合总是会分成两类:选中的,和没有选中的。
(划分的条件在集合论里面叫等价关系。我尽量的不扯这些,但还是希望我的依据能被了解)
在本例中,有一个科目本身的树状结构,
未来可能还有部门的结构,他们都是树状的。
树状结构是偏序关系,我们尽力的在加上约束条件,把偏序关系破坏掉,变成等价关系。
这样就可以形成我们所要的一组划分。
(如果不这样,我敢保证,你没有办法从逻辑上作统计。)
形象的说:我们可以认为,不管是科目结构还是部门结构,都是对
所有的科目的一种view,这两个view是独立的,从这两个view里面可以看到科目的元素的集合。
这两个科目的集合的交集中的元素的和,就是我们想要的结果。
(如果不了解view的概念,可以参考数据库的view的概念)
我们对科目进行一次划分,总是有些划分的根据的,那些根据可能是科目的属性(或者属性的组合),
也可能是与科目相关联的关系。
(lz所说的那些某些特殊科目,总有些东西标明他们是特殊科目,要么是属性,要么是属性组合,
要么是关联的关系。
如果是强关联,那么可以作为一个属性,如果是弱关联,那么可以分解为关系类,或者引用or指针-cpp或者java中才有的。)
好了,忽悠了半天,云里雾里的。
我们来看看怎么样用oo来设计吧。因为是小case,就省略去一系列的
系统交互图到类图再到交互图,还有分析哪些类的状态的步骤。
直奔主体--类图。
account 类 属性若干
department类 account的关联类,如果对cpp了解
view1类 建立科目结构,并按照这个某些过滤条件进行筛选。
view2类 建立部门的结构,并按照这个某些过滤条件进行筛选。
view类
setTarget( account array) //需要划分的集合
setFilter(filters ) //可以把判断条件委托给filter类,
getResult() //得到被选中的元素的集合,这里封装了遍历的逻辑,
//和利用filter的判断的逻辑可以完成筛选。
汇总类 把各个view实例的结果集合,按照自己的规则进行加总。
//如果汇总不涉及到被其他的逻辑引用,可以用函数实现
饿得慌,先忽悠到这里吧。这个模型还不是太完善。
但是有一点可以肯定,gp在这场比拼中占不了什么优势。顶多是效率上的。
但是在多数的mis中,效率并不太受关注。
[解决办法]
关于concept,很多人都以为跟interface是一个概念,主要是用来对GP的类型进行约束的,也会带来名字依赖,我认为这是一种误解。下面是我前一段时间跟的帖子。
个人认为concept和interface完全不同,虽然表面上看起来两者是完全相同的东西。或许non-intrusive的interface的说法有一些相似之处。
对interface来说。
首先,如果某个方法依赖于interface对参数类型进行约束,那么首先传进来的参数必须实现该interface的接口的一个类。
也就是它先要量明身份。这样对对传入的参数首先有个限制,该参数必须显示声明自己实现了某个接口(注意是显示声明不是实现,光有实现还不太好使,一定要加上implements、: public、:等字样 )。如果你事先没有考虑周全,那么后期要么你是改类的定义,让它实现这一个接口,要么就是定义一个adapter,来实现这个接口,然后让你原来的成为一个adaptee。
第二点,interface作为一个实现它的类的操作界面,这里必须经过虚函数进行动态的调用。这个可以实现运行时多态,是好事,但也同事也可能是坏事。
对concept来说,事情完全不同。
首先,正如它的名字一样,它是一个concept。一般来讲,如果是auto,则编译器在编译的时候自动来检测传入参数是否具备了他所需要的concept,即一系列方法。如果客户对象具备了方法签名的要求,那么OK,否则编译器将给出相应的告警信息,比没有concept的时候的告警信息要清晰得多。跟interface不同的是,它一般不需要传入的对象是某个接口类型,相应的就少了好多adapter的使用。
第二,同样如它的名字所示,它只是在编译器的一个规格检查,并不参与到运行时的代码中。跟interface不同的是你无法用一个指向concept的指针在运行时通过虚函数进行多态的操作。由于这个原因,concept的实现就是一个规格清单,检验完了就扔掉了,它不需要卷入虚函数的漩涡,甚至不会影响二进制代码。
对于interface和concept来说,无所谓谁优谁劣。他们产生的目的就不一样。尽管都有约束,但是concept的目的主要是服务于泛型实践,使编译器能够更快更好的检测客户类型是否符合规范,并且给出更好的错误信息说明,而interface主要是为了运行时有一样的内存模型,使得多态操作得以施行