C++杂思录——风格的选择

    技术2022-05-11  174

    风格的选择

    【警告】我目前从事嵌入式开发,文章中的观点受到我浅薄开发经验的强烈影响,各位请抱着批判的观点看待。另外,此文的以项目实际开发为衡量标准,请不要以仅在理论中存在的理想标准来评价本文的观点。

    【大师语录】

    Herb Sutter(1998):...在我们公司的开发中,大量使用的是封装,包容,访问控制,ADT,不大用继承和多态。我们经常使用STL,template...至于异常,我通常都用new (nothrow) Classname!;-)  ...

    Douglas Lea(1995): 也许对C++最好的看法是把它看成一系列较小、较简单的语言。包括:一种接口定义语言,一种数据抽象语言,一种静态类型的面向对象语言,一种过程化语言(也就是C)。它的每个方面都有一些缺陷...,让这些模型和特性彼此交互协作会导致严重的复杂性。通常,对付这种复杂性的最佳手段是始终坚持使用少量的几个设计和编程惯用技术(就是今天所谓的模式——译者),这些技术又组合成为更加通用和有效的技术。(设计开发中的)一个首要目标是尽可能远离这个语言中的阴暗角落。

    【正文】

        Bjarne Stroustrup说,C++有四个部分:better C,ADT,OO,和GP。虽然现在也有一些新的风格被证明可以在C++中运用,比如functional, generative,meta programming等等,但是在实际工程中,主流的风格就是这四种。我们通常使用OO风格进行软件开发,但是,严格来说,目前C++中的所谓OO风格是一种典型的混合风格。

        是不是可以这样认为,C++实际上是几种不同风格的语言集合。也就是说,你可以把它看成几种不同的语言,可以只使用其中一种语言进行完整的软件开发。如果同时杂合使用一种以上的风格,则复杂性会大幅度地增加。我个人认为,这是C++在实践中难于控制的一个主要原因。混合使用不同风格,就好像在一个源文件里混合使用多种不同的语言,复杂和不一致性必然暴露。当然,C++独特的魅力正在于混合风格编程的强大威力。这正是一把双刃剑,虽然具有潜在的强大威力,但是通常来说也是导致项目混乱的重要原因。

        我认为以下面的原则进行实际开发,将可以在一定程度上规避风险:

    1). 在任何一个单个的时间点,只使用一种编程风格。

    2). 以一种风格为主风格,用它来组织整体模块的开发。

    3). 在遇到特别适合另一种风格的典型场景,可以用一个子模块包装该场景,然后在该子模块中使用该风格,但记住遵循要求1,避免混合风格。此外,必须通过封装手段将该模块包装起来,以符合主体风格的要求。比如说,主风格是better C,在某个子模块中用到了面向对象,则应当使这个子模块从整体上看来像是一个普通的C过程。

    4). 在个别场合,混合风格的确有很大的好处。但是,这种情形是比较少见的,一般来说比较成功的实践已经总结成patterns,所以在工程实际中,可以强行规定,只有在符合某个patterns的情况下才可以谨慎地使用混合风格,严禁擅自创造新的混合用法。

        现在来讨论一下究竟应当如何划分C++风格。Stroustrup对C++风格的分类是从语言开发者的角度进行的。如果我们以下面的原则进行分类,我认为会得出不同的结果:

    1) 每一种风格必须构成一个完整的子语言,具有完备性,可以单独使用这一子语言开发任何软件系统,有经过历史验证的成功经验。

    2) 每一种风格必须相对简单,有一致的、简单的、得到认可和验证的原则。

    3) 每一种子语言必须能够在现实世界中找到相对应的其他语言。

        依据以上原则,我将C++划分为三个半子语言:

    1)  Better C, 只增加函数重载、引用类型、缺省参数等简单特性的类C子集。对应ANSI C语言。

    2)  ADT C++,即C with Class,整个程序由平面化的具体类(concrete class)对象构成,无继承,无多态。对应Ada 83语言。

    3) IDL C++,我称之为Interface-Oriented,典型范例是COM组件模型。

    3.5) GP C++, 利用模板技术形成了一种库和组件的实现语言。这不是一种完整子语言,一方面因为可以把它看成是ADT C++的一种延伸,另一方面它必须依附于其他风格而发挥作用。

        显然,我这里遗留了一个最重要的风格,也就是我们通常所说的“传统面向对象”风格,由Smalltalk,Java等语言所展示的,由MFC等类库经过多年实践论证了的一种风格:靠庞大的继承树抽象和组织各种数据类型,靠继承和组合实现代码复用。这种风格为什么没有被我提及呢?

        因为我认为这种风格实际上是一种混合风格!可以认为是在试图融合上述第2、3和3.5种风格。在前述的三条原则里,它严重地违背了第二条。由于C++的静态本质,由于C++缺乏天然的类库和垃圾收集机制,使得在C++中进行Smalltalk风格的编程非常非常困难,以至于为了克服这些困难,C++实际上发展出了一套不同于Smalltalk、Java风格的独特的“面向对象”编程风格。这套风格历经近15年实践,应该说有成功有失败,虽然出版了大量的著作,至今没有形成简单的、一致的、可仿效的风格指导。从某种意义上说,如此多的C++面向对象编程指导书籍十几年常盛不衰,恰恰说明这种风格的困难程度和难以仿效性。就我个人而言,我已经不再以这种风格为指导思想了。我不会再拼命地构造继承树,思考哪些函数应该是虚函数这类问题了。

        你可以认为“为了复用代码而进行的继承”是这种风格的标志。请注意,ADT C++允许组合,对于继承则应该想尽一切办法避免。而IDL C++的典型代表COM,根本就不支持这种继承,它支持的只是接口的复用。

        当然,这并不是要否定十几年来C++在面向对象方面发展的成绩。但是,如果你现在从头开始规划一个完整的项目,那么我认为如果选择这种杂合风格,是不太明智的。但是这种风格也有两个典型的使用场景:

    1) 有一个完整的框架支持。比如MFC。虽然这种风格本身有很多技术难点,但是MFC这样的框架已经帮你克服了一部分,给你营造了一个类似Smalltalk那样的、相对舒适环境,这时候可以使用这种风格。但是通常要认识到,这类框架在克服不少技术难点的同时,引入了一些新的问题,有时是更加难以对付的问题,所以要明智,并且做好充分准备。

    2) 符合经典模式。如果遇到某个典型的“面向对象”场景,已经有了成熟的、优秀的、现成的、文档化了的设计解决方案,则可以有选择的、谨慎地使用之。我指的主要就是GoF和其他一些设计模式。这里所谓的“经典模式”数量绝对不会太多,但是却大量地、反复地出现在设计中,并且往往复合出现。这样的情况用已经经过验证的设计方案来解决是非常合适的。我个人在这里有一些实践,觉得应该注意几个问题。第一是要谨慎,我遇到过大量的情形,看上去很适合用某个模式来解决,但是真的用了才发现并不是这么回事。在不适合的地方套用了错误的模式,会把事情弄得一团糟;二是最好将设计方案局部化,包装起来,从外面看不出你使用了什么模式。三是注意内存问题。使用OO风格的最大障碍其实就是内存问题。

        其实这个口子一开,最终的设计里仍然会出现大量的“传统OO”风格,因为经典模式实在是太普遍了。所以主要问题是控制和包装,因为四处泛滥的模式实际上等于重新回到混合风格。

        值得指出的是,其实在大部分的经典模式里,并不存在“为了复用代码而进行的继承”。我们可以认为,凡是合理的面向对象,必然具有接口继承的特征,必然出现抽象类,很可能出现多态包容。

        就我个人而言,由于从事嵌入式应用软件和高层系统软件的开发,出于嵌入式系统对于效率的极端关注,我比较倾向于下面的组合:

    1) 以ADT C++作为主风格。

    2) 利用GP辅助设计良好的ADT。以GP和组合实现代码复用。

    3) 将可能的OO包装在平面式的类中。

    4) 禁止使用异常。这一点将会有专门的反思文章。

        对于PC和大型项目的开发,我觉得以IDL C++的风格作为主程序应该是更合适的。目前我还没有这方面的实践。但是我希望能够尽快有一些尝试。

    结语:    有人可能会质疑我对“复用性”的重视不足。因为传统OO的一大立足点。对此我不予否认。我认为目前很多程序员对复用性问题不是考虑不足,而是考虑过度。尤其是应用程序员,花费太多的精力去让自己的组件满足未来可能的需求变化,很可能是在浪费时间。对于“复用性”这个话题,我也有一些想法,容后再述。

    预想中的论题:

    1. 禁止异常——在没有异常的C++中生存

    2. 复用性——三思而行

    3. “标准组件”的误区——适当恢复“自己动手,丰衣足食”的传统


    最新回复(0)