优雅和其他设计理念
蒋贤哲 译
在编码之前思考
Bill Venners(以下简称Bill):在与Biltek的一次访谈中,你说,“我不是使用支持工具进行巧妙设计的信徒。然而,对于系统性使用数据抽象、OOP、泛型编程我则是一个坚定的支持者。那些只是坐下来编写一页页代码,而没有全局性设计、且不支持类和模板的人只不过是在浪费时间和制造不必要的维护问题。”那么你将推荐何种程度的前瞻性设计,在编码之前我们应该思考多少呢?
Bjarne Stroustrup(以下简称BS):这在很大程度上取决于问题的规模。如果你须编写一个今天下午所需的小型程序,那么设计将可能只需要信封背面那么大的地方。如果你正在编写一个必须运行多年并需耗费多年 才能创建成的系统,那么很明显,必须做大量的前瞻性设计。如果项目牵涉了大量人员,那么就须作更多的前瞻性设计。但是,系统规模越大,前瞻性设计也将变得愈加困难。设计工具并没有为你提供多少反馈,所以我倾向于这样的观点,即你应当先编写一个小型系统,然后再逐渐将其扩展为一个比较大的系统。规律表明非常成功的大型系统都是由正在运行的相对小 的系统逐步发展而来。你可以反复地运用这一规则。
一些人可能会认为我是在谈论创建原型。尽管在一定程度上如此,但是这并不是我所谈论的全部,因为原型也可能是一个陷阱。如果你是雇佣一种不同类型的开发人员、使用一种不同的工具、以及与实际应用程序完全不同的工作量来创建一个原型,那么你就可能遭遇真正的难题。例如,在开发一个支持十个用户的原型中,你可能需要每个人负担更多的硬件 和软件,需要更多的有经验的开发人员,而与原本开发一个必须支持一万个用户的原型相比,前者的负担要多于后者。通常你可以忽略了一个原型中那些不怎么方便的标准和兼容性问题。当然,此时还有一些方法中的问题没有膨胀起来。
有时你可以创建一个系统的部分功能以便对其进行测试。我个人则非常倾向于尽早进行整合测试。例如,在一个分布式系统中,你可决定使用何种工具,并编写“Hello, world”的逻辑上的等价程序,你可以从系统各个不同部分的应用程序中的最小例子开始进行测试,并让它们互相进行测试和通信。就是此刻,你会发现你的JAVA ORB不能和你的C++ ORB进行通信,或是HP上的JAVA虚拟机不能和SUN上的JAVA虚拟机进行通信,或是其他任何可能发生的事情。很明显,此类事情是从不应该发生的,但它们的确存在。可能不仅仅是我在此处所提到的这些特定问题,而且可能是一些经常发生但又未曾预料到的事情。我现在不是在仅仅讨论原型 ,而是在讨论测试并对最终都须工作的所有内容进行测试要及早对它们进行测试。
库的编程思想
Bill:我经常听到你阐述C++支持库的设计。你认为在何种程度上“那些正在创建应用程序的程序员应当自认为是库的设计者”?我们应当将自己的应用程序划分为这个库或是那个库吗?或是当我们在创建一个供他人使用的库时仅仅应当以库的有关概念进行思考吗?
BS: 我认为人们应当在内心将编程划分为创建库与使用库两个部分。今天你可编写一个支持库,那么明天你就可以使用它。绝大多数情况下,人们都将他们的系统作为一个庞大的聚合体进行测试和创建。即使创建一个在短时间内运行的单个程序,我都倾向于创建一个支持的函数和类,而且我将它们设计为是受到支持的。我不是直接去创建能够完成整个任务的所有事情。所以,无论名义上你是一个库的设计者或是一个应用程序设计者,你都应当考虑如何能够将系统的不同逻辑部分划分开来。
其中有关C++的一个问题是尽管存在大量的库,但是没有任何可供人们寻找所需的库以及库的相关信息的 论坛或会场。例如,有关C++和GUI的问题不是没有C++ GUI库,问题是存在大约25个合理的C++ GUI库,而且其中一些还非常不错,但人们会说,“哦,没有C++ GUI库。它不符合标准。”例如,C++没有类似于针对于Python的公共会议场所,在此你知道自己能够找到所需的Python库。这是一个充斥着财富、且数量巨大、但缺少市场的问题。C++圈子里面似乎没有人能够拥有这么多的 经费来支持这样一个公共论坛。
你不用编写库的一个方法就是认为你自己是伟大的库设计者。你为我提供库。我使用之,你就此离开去做别的事情。你不得不认为一个库是你自己正在使用的一些东西,而且是一些“你还须作为一个对其较长阶段支持者”的事情。你必须和用户进行充分交流以便获取正确的抽象和正确的实际细节。一个库必须不断演化。最好的库是有那些也作为其用户的人创建的。你不可能在设计者和用户之间划分一个很明显的界限。
如何取舍
Bill:C++内部拥有很多东西。一个设计者置入一个语言或是库中的特性越多,用户就能够获得越多的功能,但是他们也得到了更多的 接口。即使明明知晓他们希望忽略某些特性,用户也必须知晓它们。作为一个语言或库的设计者,你如何决定特性的取舍?
BS:这非常困难。我对此并没有一个足够好的哲学基础,我想其他人也一样。在C++语言里面,我试着将那些会帮助所有人而不是仅仅会帮助某一特定团体的内容加入。我试着加入一些具有较大普遍性的机制。例如,我没有加进那些有关GUI的特定特征,因为大部分人不会将GUI放在他们的程序中。我没有将有关数据库的特定特性加进 来,因为大部分人不使用数据库。我没有将并发的特性加进来,因为你可将它们置入库中。所以我选择了通用性,那些理论上将会为所有人(直接或是间接的)用到的特性。而且我试着不强迫人们去做大量重复、单调的事情。 我为了消除危险转型和重复而付出的有关模板、内联和重载工作也许是最好的例子。
当然,你可以说我刚才所说的可适用于一个语言自身的设计或大家都在使用(或应当使用)的一个标准库设计,而且很难定义一种语言及其库之间的明显界线。在C++中,我倾向于加进那些是抽象机制的语言机制。所以和其他一些语言不同的是,我没有加进一个高级的 向量,因为可以籍由语言提供的特性在库中完成这一操作。我没有提供一个“带有支持字符串操作”的特定机制的字符串。相反,我加入了允许你编写一个字符串类的通用机制。我没有加进属性是因为你能够编写一个属性类。所以在此程度上,我反对其他一些语言中当前的“将一些特定和特殊的机制加 进来”的流行趋势。我想这是我的一个基本哲学理念。
如果你想获取更多的细节,请参考《The Design and Evolution of C++》,其中我记录了C++为什么会以它现在的方式进行设计。除了其他事情之外,阅读这本书将会帮助你避免像其他人 (此处就是指我)那样是事后聪明而且企图回头改变历史。这本书是对我当时思考的一个相当完整的记录。在计算机世界中很遗憾地存在大量的修正主义者的故事。有意思的是,自从我在 《The Design and Evolution of C++》中的描述以后,C++语言一直没有改变,发生变化的是标准库和一些用法的风格。
自我防护
Bill:语言和库的设计者通常认为用户要自我防护,但是在C++中,你似乎更多应验着“设计者应当给人们足够的圈套以至于他们可以自我绊倒”的哲学信念。在你的C++ FAQ中,你写到,“当你使人们免于简单的危险时,他们会使自身深陷新的且不明显的问题之中。”在我们的设计中,应当如何使用户避免犯潜在的错误呢?
BS:我不像其他一些语言设计者那样悲观。我可能对有能力的用户 (此处是指程序员)该是什么样子持有比较浪漫的看法,我认为程序员不像常人那样愚蠢,而我更愿意将他们看作是受人尊敬的专业人员。我也希望程序员通常能够表现得更加专业,人们会更愿意对更加 称职的东西付费。
另外,我不希望通过提供用户一些仅仅允许他们做某类事情的机制而剥夺其权利。我经常看到,如果系统的用户想执行一些非由系统直接的功能,就要求用户求助于系统提供商 来提供新的机制。最好的例证就是一些第四代语言和那些根据模型或图表生成代码的工具。如果你所需求的一些功能没有被工具提供商所考虑,你通常需要求助于提供商 对工具加以修改。我曾看到一个客户不能轻易定义一种新的排序操作的例子。一个相关的例子就是那些在其中你不能编写标准库、更不用说高级库的语言。这就意味着你将用户群体划分为不信任的开发者团队以及为提供商工作的受信任的特权群体。
我倾向于看到每个人都是平等的。我的想法是如果你不喜欢由你提供商所提供的库,那么你可以创建自己的库,因为你拥有同样可用的工具。这一想法在各种工具和平台提供商之间似乎不是很流行。
C就是按照“你可以在语言自身当中编写其标准库”这一原则设计的 ,同一原则指导了C++的设计。我认为这并不是一条适用于其他任何语言的一个准则。其他语言所做的就是编写自己关于C或C++的实现。这一方法虽然可行,但也冒着巨大的风险:大量的无知群体在一些受保护的语言中编写代码,而那些为提供商工作的专家则在那些创建实际工作的地方编写代码。一旦你拥有通用性,你可通过编码标准或是库来取得安全 ,最好是通过由库所支持的针对于特定领域的编码标准。
无限设计
Bill:在与Rogue Wave进行的一次采访中,你说,“我不希望创建一个只能执行我能想到的东西的工具。”
BS:是的。
Bill:我们如何能够做到这一点?我们如何针对那些我们未曾想到的情形进行设计?
BS:你聆听了大量人的讲话,那么你就会以通用性为目标。
Bill:通用性对特定问题是否意味着更加抽象?或是你是指大部分人将会使用的机制?请具体给通用性下一个定义。
BS:让我们举一个例子。一个类几乎能够做任何事情。C++没有单独的语言构件将类指定于大对象、小对象、面向对象的类、数值类型、GUI事件、属性 以及线程等等。C++类的概念具有足够的通用性来使用户指定所有这些内容,而且以高效的方法完成。在C++设计中一个有关通用性的例子就是,我没有因为调用它们非常 繁杂而让类仅仅适用于大型的繁杂操作。
除了在“你不再限制用户”这一层面上的通用性外,你也可以专注于数学层面上的通用性。你要确保所有这些情形都能运作,而且是整个逻辑设计都被涵盖,而不是说你仅仅能够做到这五个特性,而其余都不能涉及。为什么只能是五个特性呢?当你以一列特性或是一列替代物进行设计时,你将不可能得到正确的设计。这通常被证明是大部分情形,而不仅仅是个别情形。例如,在C++中没有提供针对于某个特定操作系统处理特定事件的内置机制 ,但提供了通用机制由此你可以编写一个类来处理针对某个特定操作系统的事件。
不成熟的通用性
Bill:再回到以库进行考虑的应用程序设计者这一话题,他们何时应当采用通用性机制而不是以具体方法来解决他们面临的问题呢?
BS:我真的不知道。这是一个 太广泛的问题,一个忠实的答案必须包含“有时”和“它依赖于……”。对于早前自己没有尝试过几次的问题,我倾向于创建一个特定的方案,因为从根本上来讲理解具体的解决方案要胜于抽象。当我再次遇到同一问题,并再次处于解决之中,我说,“等一下,我正在再次做同一问题。”我已经做过一次,我已经理解了它 ,我已经知道在前一方案中存在的缺点。现在是回顾过去以便看看自己能否找到一个通用方案的时候了。这一方法和那些从未尝试一次就开始寻求通用性的人们的方法相反 ,他们坐下来试图针对自己从未有过经验的问题设计一个通用方案。
Bill:这听起来似乎是未成熟的通用性。
BS:是的,正是如此。就如未成熟的优化一样,你也会拥有未成熟的通用性。
针对拙劣操作的拙劣语法
Bill:在你的C++ Style and Technique FAQ中,你说,“一个拙劣的操作应当具有一个拙劣的语法形式。”我认为这是一个有趣的设计方法。你想规劝用户不要做一些事情,但是当他们的确需要去做的时候,你又不想使这不可能,所以尽管你提供了一个方法,但是使其很拙劣。
BS:是的。这是新的转型语法。转型提供了一个本质性的服务。这一新的语法是对C风格转型的一个改进,因为它允许编译器来检查那些旧语法不能应对的错误。但是新语法是有意做成是拙劣的,因为转型依旧是一个拙劣并通常是不安全的操作。
优雅和其他设计理念
Bill:在我们进行设计时,我们的理念和目标应是什么?你经常谈论到“优雅”。那么对优雅的投入又有何回报呢,或是你认为在设计方面我们的目标是什么?
BS:“优雅”与“简单”是两个联系非常紧密的单词。“可理解”是这一领域内的另外一个单词。它们意味着什么呢?它接近于:你可针对你的程序运用工具,你可对其进行优化,你可维护它,你可移植它。如果你能够从逻辑上标识出某些东西,你就能够 对付它。如果它仅仅只是散落在一个大程序中的代码,你就不可能用它来做一件事情。
顺便说一下,最为优雅的事情之一就是可声明性。这也是我喜欢静态类型语言的原因之一。你可以说,这是一个具有双精度浮点数的矩阵。那么现在你知晓了很多东西,你知晓了乘法对其意味着什么,且拥有关于矩阵的一整套理论。当你声明了一个带有构造函数和析构函数的类并创建一个对象 后,就等于声明了“资源将会在析构函数中释放”。这非常清晰,它就在那儿,人们可以理解。人和工具都可以做事情。
当我使用优雅时……
Bill:是的,优雅意味着什么呢?
BS:这个比较难讲。如果你看到了数学中的一个证明,你就能够说它是优雅的。通常情况下,它是你所能做到的清晰和简短。除了其他事情之外,我曾经想被培训为一个数学家,我认为这 中理念始终指导着我的思想。此处有一点美学的观点,但是美学与人们真正喜欢的东西(诸如维护性与非常快的开发)联系非常紧密,因为你现在是工作在一个比较高的抽象层面并在使用较高层面的构件 。如果你需要的话,还有效率问题。
我不停地告诫我的学生他们应当懒惰一点。我拒绝太长的程序,因为当学生真正开始工作时,他们将不会有时间来编写这么长的代码。你会发现与其他替代物相比,我倡导 的许多优雅的东西都是比较短的。令人吃惊的事情之一就是当将优良的C++代码与其他语言的优良代码相比时,C++代码通常都是比较短。当以其他类似标准库或其他优良的库的东西编写代码时,C++代码都是比较短的,因为你可以非常 简洁地表达想法。这是我的理念之一。说你想说的事情,清晰地说,说得尽可能具有普遍性,你将发现生产的代码相对简短并可能运行得飞快。
评论