泛式(paradigm)就是编程的一种思维模式,比如面向过程,基于对象,面向对象,泛型,生成型(generative programming)。
面向对象(OO)和泛型(Generic)是目前程序设计方法中最受关注的两颗银弹,不过Frederick Phillips Brooks早就告诉过我们,No Silver!【注1】想要对付软件设计中具体而又频繁,狰狞而又可恶的人狼,那还是需要大量人的经验以及目前计算机所不具有的智慧。不过面向对象和泛型虽然还不足于堪当万灵银弹,但是它们仍然带给我们很多新的理念,新的解决软件人狼的办法。在实际的软件项目中,它们也已经成功的证明了自己。
注1:其实什么问题又有绝对完美的解决办法呢?这个道理谁都明白,但能够像《人月神话》那样畅销已经快30年,还在继续畅销,能够像Brooks那样将问题讲的让人信服,妙语如珠,畅快淋漓,达到技术之巧与人文之美的几近完美结合,就不是那么容易的了。不过我一直认为翻译是很难的,语言之间的习惯,表达方式相差太大,虽然中文版翻译的非常好(至少我现在是望尘莫及)了,但我还是觉得英文的味道是不好翻译的,建议可以同时买一本英文影印版一起看,更能领略英语的文字之美。
说到面向对象,现在已经不再是新的概念了,虽然很多人言必说面向对象,但是真正知道并会灵活运用封装,继承,多态的实在太少。
面向对象是一个纵向垂直的概念,也就是采用平面几何中的Y轴方向对软件元素的一种划分。它将要解决的问题域空间中的问题映射成一个个对象(object),然后将各种各样的对象进行归类,抽象出各种类型(class or type)以及类型之间的各种关系,最一般的关系就是继承,比较高级的就要用到各种设计模式(design pattern)来进一步刻划对象之间和类型之间的关系,增加“额外间接层(extra level of indirection)”,封装变化,分离界面与实现,从而采用的更灵活,更容易维护的设计。
虽然继承是我们接触面向对象最先知道的东西,但是却有一个很简明的判断标准:如果一个面向对象的软件用到了大量和很深的继承关系,那几乎毫无疑问是一个失败的设计,继承主要是用来尽可能的消除程序中的if-else、switch,实现多态,仅仅为了复用代码采用继承那是非常不明智的行为,它只是把更多的麻烦留给不远的未来。要尽可能的采用组合而不是继承(C++中的保护继承和私有继承可以说是一种组合,而不能叫继承)。要用继承,必须要考虑到Liskov原则,很简单的来说,用到父类型的地方,子类型能畅通无阻的使用,也即对于父类型的所有界面(interface)【注2】,子类型都实现了。
注2:也有翻译为接口的,不过我更喜欢界面。Interface也有人称其为Protocol(协议)的,所谓协议,也即提供的一组服务,在计算机网络中用到的最多,在面向对象中就被实现为类型的成员函数(或者叫方法)。
所谓面向对象,封装,继承,多态,要将其理解,并不太难,我介绍一本书,《Thinking in Java》,目前最新是第三版。而且它也是学习Java的一本好书,可以用一个星期(7天)的时间好好看一看,基本的Java语言知识和面向对象的基本概念就一定会谙熟于心。【注3】
注3:在去年(2002年)暑假的时候,我当时为了应聘一个兼职,用不到两天的时间(大概总共12个小时左右)在电脑上将《Thinking in Java》英文第二版看了一大半,并将所有的程序调试通过,感觉很容易上手。只是可惜由于公司的原因,兼职没有去成。当时我的计算机基础不怎么样,自学计算机也还不到一年,学了大概4个月的C++和C(我是学C++的过程中学会C的),而且本人也不怎么聪明,所以我认为你也一定可以7天看完3遍。这本书的英文很浅显,基本上没有什么难句,说理很清晰明白,可惜没有看到什么英文语言之美,这方面感觉不如《Effective C++》和《More Effective C++》,更不用提《The Mythical Man-Month》了。但作为一本Java语言入门教科书那是非常好了。
面向对象的运用与研究目前基本上已经达到了它所能到达的极限,当红明星Java与C#只支持面向对象一种设计范式(paradigm)【注4】就成功的证明了面向对象的无穷威力,感觉现在在IT媒体就像始皇帝的江山一统【注5】,即使对面向对象来说并不习惯的表达方式,也已经被许多天才横溢的程序员用面向对象实现了。但是削足适履让人并不舒服。比如对程序中“横切关注点(crosscutting concern)”的实现用面向对象来设计就是非常别扭的,一种新的设计范式面向方面(Aspect Oriented)应运而生,并在AspectJ中得到实现。【注6】
注4:你一定会说:“Stop,你遗漏了一点,Java还有Generic Java,它还支持泛型编程。”但请注意两点:
A. Generic Java并不是由官方支持的,至少目前它还没有能够溶入正统的Java血液之中。
B. Generic Java的泛型实现采用的是擦拭法(Erasure),也就是由编译器将泛型代码改头换面为面向对象代码,最后在编译为.class文件。可以参考《程序员》杂志2002.8-2002.9期,侯捷先生的《Java泛型技术之发展》。
注5:“夷平六国是谁?哪个统一称霸?谁人战绩高过孤家?高高在上,诸君看吧,朕之江山美好如画。登高踏步,指天笑骂,秦是始,人在此,夺了万世潇洒!” ——《秦汉建筑》。
注6:请参考《程序员》2002.11期的AOP专栏。
AOP是一个比较新的设计泛式,我也只看了几篇文章,不敢胡乱发表意见,有兴趣的读者可以到http://aosd.net去领略一次异境之旅。
言归正传,面向对象编程语言对类型的语言支持非常丰富,有encapsulation、class、interface、inheritance、polymorphism、aggregate、RTTI。现实生活中问题域中的各种概念在解域空间中有了很好的映射,所以具体用程序语言来实现就比较轻松了。
可惜完美就像天堂一样,可以想象,但不能达到。我们的空间也不是只有一维,有许多问题用面向对象的纵向垂直Y轴是独木难成林,即使实现了也是一种违反常识的蹩脚方法。比如ADT(Abstrct Data Type)的实现。具体比如栈(Stack),它表现的是体现一组功能(先进后出,弹出,压入)的集合。它的所有具现物(对象)并不需要有一个共同的基类(base class),具现物可以彼此至死不相往来,但在面向对象中实现却必须有一个共同的基类,然后每个具现物的类型都从这个基类继承,有多少种不同具现物,就有多少个类。下面一段简短的代码表现的更清晰:
class Stack
{
void push();
void pop();
Stack top();
//……
};
class IntStack : public Stack
{
……
};
class PlateStack : public Stack
{
……
};
//许多不同类型的Stack类。
不过在Java中几乎所有类都是从Object class继承,它单独实现了一个Stack类,可以将各种类型的对象压入Stack,取出的时候再由Object向下转型为具体类。
示例代码:
//Java代码
Stack s = new Stack();
s.push(“string”);
String str = (String)s.top();
这是以损失效率和类型安全为代价的。
总之,这两种解决方法都很扭曲!如果能够像这样用stack<int>,stack<string>,……就好了,效率的熊掌、类型安全的鱼都得到了。而面向对象缺少的就是这样一种对软件组件的横向切分。它们有同样的ADT,但除此之外,可以没有任何联系,Y轴与X轴是正交的,所以Y轴不可能实现X轴的功能。
这个时候就应该轮到姗姗来迟的泛型X轴粉墨登场了。C++中的STL就是这一轴的杰出成就。请记住,模板(template)并不是泛型,只是C++中的泛型采用了模板来实现。STL中用模板实现了各种常用算法和数据结构,并保证了效率的高效,比你自己实现要安全的多和快的多(一般来说是这样的),现在的C++程序员比以前要幸福的多,再也不用一遍又一遍的重复造轮子了。
但是在泛型设计的洪荒年代(这个年代可以说到现在还没有结束),用泛型技术实现的代码异常的难于调试。软件总会是有错误的,程序员所能做的就是尽快的找到bug,并修正它。由于泛型技术出现的时间并不太长,而且受到的礼遇是远远不如面向对象,所以它到现在仍然不是很成熟。语言本身缺少很多对它的支持。
面向对象中的一些概念,Interface(C++中用ABC,Abstract Base Class来模拟),Instantiation,Implementation(extends in Java,inheritance in C++)。在语言级(language level)都得到了很好的支持,所谓语言级支持就是:比如你实现的函数参数类型不匹配的话,例如对于签名(signature)为void fun(ClassA a);的函数,你这样调用它
ClassB b;
fun(b);
编译器会给你很清晰的报错“conversion from `ClassB' to type `ClassA' requested, type is mismatched.”
但是对应于面向对象中的这些概念,泛型也有相应的一些概念,下面我用泛型中的几个学术术语,对应于Interface的是Concept,在下面的例子中
template <typename T>
class Stack;
就像上例中的fun函数引数(argument)必须符合类ClassA的Interface一样,T必须符合一个Concept——AssignableConcept。也就是说模板类型参数T必须是可以赋值(assignable)而不变的对象类型,你也许会奇怪,还有不可赋值的东西,有的,具有拥有权(owership)转移的智能指针(smart pointer)就不行,恰好标准中唯一的一个智能指针auto_ptr就是这样。Concept可以说是T的种类(category),T必须满足Concept的要求。
对应于Instantiation的泛型术语叫Model,Instantiation是Interface的具体类的具现体,也就是一个个具体对象(object),Model就是Concept的具现体。比如说int,double,string类型都是LessThanComparableConcept的具现体,就像100,200是类型int的具现体,3.4,2.6是类型double的具现体,请再好好体会一下。
对应于Implementation的是Finement(细化,精化)【注7】,比如对于ConceptIterator, ForwardIterator和RandomIterator都是它的Model,但RandomIterator本身也是ForwardIterator,反过来则不行,所以说RandomIterator是ForwardIterator的一个Finement。
注7:我总觉得这个对应不太恰当,但想不出一个很好的对应名字。其实Finement相当于一个深类层次(deep class hierarchy),靠近底层的具体类是上层类的细化。比如类层次:
|---------------------|
|-----------A--------|
| |
|---------------------|
|
\|/
|---------------------|
|-----------B--------|
| |
|---------------------|
|
\|/
|---------------------|
|-----------C--------|
| |
|---------------------|
class C就是class B的细化。一直比较遗憾很想看看Matthew H. Austern的名著《泛型编程与 STL》,但是以前是买不到,现在是手中无米,只有以后上班再说了,听说里面讲到了很多泛型的理论,不知道里面对Finement有没有什么恰当的对应。
不过可惜的是,在编译器的实现中,对于这些概念Concept,Model,Finement并没有相应的检查机制,即缺乏语言本身的支持。所以这些概念就需要程序员本身来维护,如果在程序中由于这些概念本身的错误而导致程序出错的话,编译器会莫名其妙的报错:某某函数没有实现,某某标识符(identifier)不认识,……为找到真正的错误,有时候甚至会耗尽你的精力。比如GCC STL的算法实现中对于stable_sort函数的实现:
template<typename _RandomAccessIter>
inline void
stable_sort(_RandomAccessIter __first, _RandomAccessIter __last)
{…}
stable_sort函数需要两个_RandomAccessIter(随机访问迭代器,比如典型的数组指针),所以它不能用来对list进行排序。List是基于结点(node_based)的容器(container),也就是说是由一个一个的结点(node)串起来的,它的迭代器概念是_BidirectionalIter(双向迭代器)。所以std::stable_sort()函数不能对list进行排序,但这样用:
std::list<int> li;
…//对li充实内容
//试图对li排序
std::stable_sort( li.begin(), li.end() );//错误,应该用li.sort();
编译器应该对Concept mismatch报错,老一点的编译器都不能找到这个原因。在著名的B面向对象st库中Jeremy Siek实现了一个ConceptCheck库http://www.b面向对象st.org/libs/concept_check/concept_check.htm,专门用于此类错误的检查,GCC STL就直接采用了这个库,VC7.1中也自己实现了类似的库,现在的报错就比较明显了【注8】。但是这类concept检查就应该由语言本身来检查,而不应该借助于外力(专门的库和大量丑陋的宏),所以说泛型的设计范式(paradigm)还不成熟。
注8:需要寻求STL相关的错误准确定位,可以参考Leor Zolman's STLFilt 错误消息过滤器http://www.bdsoft.com/t面向对象ls/stlfilt.html
但是如果因为它不成熟就不去用它,那它永远也不会成熟,面向对象的设计泛式不也是这样一步一步走向成熟成功的吗?况且泛型作为对软件元素水平横向X轴的刻划,它的潜在威力就应该和垂直纵向Y轴一样大,所以即使现在如日中天的Java,C#也都准备在下一版本加入对泛型的支持。毕竟两只脚走路还是比一只脚要舒服的多:)
现在X轴,Y轴都有了,已经构成了一个完整的欧几里得(Euclid)平面。那是不是还应该有Z轴,甚至除了三维之外,还有n维,欧几里得平面之外的非欧几何?请原谅我的愚钝和无知,这些问题与思索就留给勇敢聪明而又富有想象力的读者你了。
后记:
CSDN上有一篇文章《类型——OOP和GP的核心概念》将面向对象和泛型讲的很清楚,有兴趣的读者可以看看http://www.csdn.net/Develop/Read_Article.asp?Id=14824。
最开始我想用的划分是Matrix(矩阵,母体),它只需要行(row)和列(column)就可以表示所有。但我不敢说泛型和面向对象就是软件Matrix中的行和列,要找到这两颗明珠,实在远非我力之所逮!在下一篇文章我想说说C++中的三位一体(trinity)——关于copy ctor,assignment,destructor。
吴桐写于2003.5.29
最近修改2003.6.16
本文地址:http://com.8s8s.com/it/it29225.htm