什么是软件设计?

面向对象技术,尤其是C++,似乎给软件行业带来了不小的震动。出现了大量的论文和书籍来描述如何应用这项新技术。总的来说,关于面向对象技术是否只是一个骗局的问题已经被那些关于如何以最小的努力获得利益的问题所取代。面向对象技术已经存在了一段时间,但是这种爆炸性的流行似乎有点不寻常。为什么会突然有人关注?对于这个问题,人们给出了各种各样的解释。事实上,原因可能不止一个。或许,各种因素综合起来,终于可以有所突破,这项工作正在进行中。然而,在软件革命的最新阶段,C++本身似乎是一个主要因素。同样,这个问题大概也有很多原因,但我想从一个稍微不同的角度给出一个答案:C++之所以变得流行,是因为它让软件设计变得更容易,同时也让编程变得更容易。

这个解释虽然看起来很奇怪,但却是深思熟虑的结果。本文只是想关注一下编程和编程的关系。近10年来,我一直觉得整个软件行业都没有意识到做一个软件设计和什么是真正的软件设计之间的细微差别。只要看到这一点,我想我们就能从C++成长的流行趋势中学到如何成为更好的软件工程师的深刻知识。这个知识就是编程不是构建软件,而是设计软件。

几年前,我参加了一个研讨会,讨论软件开发是不是工程学科。虽然我不记得讨论的结果,但我记得它是如何让我意识到软件行业与硬件工程做了一些错误的比较,而忽略了一些绝对正确的。其实我觉得我们不是软件工程师,因为我们没有意识到什么是真正的软件设计。现在,我更加确信这一点。

任何工程活动的最终目标都是某些类型的文档。当设计工作完成后,设计文件移交给制造团队。团队和设计团队是完全不同的群体,技能也和设计团队完全不同。如果设计文档正确地描述了一个完整的设计,那么制造团队就可以开始制造产品了。事实上,他们可以开始构建产品的许多物理对象,而无需设计师的任何进一步干预。根据我的理解回顾软件开发的生命周期后,我得出一个结论:真正符合工程设计标准的软件文档只有源代码清单。

关于这个观点,人们已经有过很多争论,赞成和反对的都足以写下无数的论文。本文假设最终的源代码是真正的软件设计,然后仔细研究这种假设带来的一些结果。我可能无法证明这个观点是正确的,但我想证明它确实解释了软件行业的一些观察到的事实,包括C++的流行。

在将代码视为软件设计的结果时,一个结果完全盖过了所有其他结果。它是非常重要和明显的,正因为如此,它完全是大多数软件组织的盲点。结果是软件的构建成本很低。它根本不具备昂贵的资质;很便宜,几乎免费。如果源代码是软件设计,那么实际的软件构造是由编译器和连接器完成的。我们经常把编译和连接一个完整的软件系统的过程称为“一次构建”。软件构建设备的主要投资非常少——所需要的只是一台计算机、一个编辑器、一个编译器和一个连接器。一旦你有了一个构建环境,实际的软件构建只需要一点时间。编译一个50000行的C++程序可能需要很长时间,但是要建立一个和50000行C++程序设计复杂度一样的硬件系统需要多久呢?

将源代码视为软件设计的另一个结果是,软件设计相对容易创建,至少在机械意义上是如此。通常只需要几天的时间就可以编写(也就是设计)出一个有代表性的软件模块(50到100行代码)(完全调试它是另一个话题,后面会详细讨论)。我很想问是否有其他学科能在这么短的时间内产生出和软件一样复杂的设计,但首先我们必须弄清楚如何衡量和比较复杂性。然而,很明显,软件设计可以很快变得非常庞大。

假设软件设计相对容易创建并且本质上构建起来不贵,那么发现软件设计通常非常庞大和复杂就不足为奇了。这似乎是显而易见的,但问题的重要性往往被忽视。学校里的项目通常有几千行代码。具有10 000行代码(设计)的软件产品也被其设计者丢弃。我们早已不再关注简单的软件。典型的商业软件的设计是由成千上万行代码组成的。许多软件设计达到数百万行代码。此外,软件设计几乎总是在进化。虽然目前的设计可能只有几千行代码,但在产品的生命周期中,你可能实际上要写很多倍的代码。

虽然有一些硬件设计看起来像软件设计一样复杂,但请注意关于现代硬件的两个事实。首先,复杂的硬件工程结果可能并不总是无错的。在这一点上,并没有我们像软件一样相信的评判标准。大多数微处理器在出售时都会出现一些逻辑错误:桥梁倒塌、大坝决堤、飞机失事以及数以千计的汽车和其他消费品被召回——所有这些都让我们记忆犹新,所有这些都是设计错误造成的结果。其次,复杂的硬件设计有一个相应的复杂和昂贵的建设阶段。因此,制造这种系统所需的能力限制了能够真正生产复杂硬件设计的公司数量。对于软件来说,没有这样的限制。目前,有数百个软件组织和数千个非常复杂的软件系统,并且数量和复杂性每天都在增加。这意味着软件行业不可能通过模仿硬件开发者来找到解决自身问题的方法。如果说有什么共同点的话,那就是当CAD和CAM可以帮助硬件设计师创造出越来越复杂的设计时,硬件工程会变得越来越类似于软件开发。

设计软件是一项管理复杂性的活动。复杂性存在于软件设计本身,存在于公司的软件组织,存在于整个软件行业。软件设计和系统设计非常相似。它可以跨越许多技术,并且经常涉及许多学科分支。软件规范通常不是固定的,并且经常快速变化,这种情况经常发生在软件设计过程中。同样,软件开发团队也往往不稳定,经常在设计过程中变动。在许多方面,软件比硬件更像一个复杂的社会或有机系统。所有这些都使得软件设计成为一个困难且容易出错的过程。虽然所有这些都不是创造性的想法,但在软件工程革命开始近30年后的今天,与其他工程行业相比,软件开发看起来仍然像是一项未经训练的技能。

人们普遍认为,当真正的工程师完成一个设计时,不管它有多复杂,他们都非常有信心这个设计能够工作。他们也非常有信心,可以使用公认的技术来构建设计。为了做到这一点,硬件工程师花费大量时间来验证和改进他们的设计。例如,考虑一个桥梁设计。在这样的设计实际建造之前,工程师将进行结构分析——他们将建立计算机模型并进行模拟,他们将建立比例模型并在风洞中或以其他方式进行测试。总之,在施工之前,设计师会用一切能想到的方法来证明设计是正确的。对于新客机的设计来说,情况更为严重;需要造一个和原来一样大小的样机,必须进行飞行试验,验证设计中的各种预期。

对于大多数人来说,软件方面显然没有硬件设计这么严格的项目。但是,如果我们把源代码看成一个设计,我们会发现,软件工程师其实已经对他们的设计做了很多验证和改进。软件工程师称之为测试和调试,而不是工程。大多数人都没有把测试和调试当成一个真正的“项目”——在软件行业肯定没有。这种观点的原因更多是因为软件行业拒绝将代码视为设计,而不是任何实际的工程差异。事实上,测试模型、原型和电路测试板已经成为其他工程学科公认的组成部分。软件设计者之所以没有或者没有使用更形式化的方法来验证自己的设计,是因为软件构建周期这一简单的经济规律。

第一个启示:仅仅构建一个设计并测试它比做其他任何事情都要便宜和简单。我们不关心构建了多少个构建——这些构建在时间上的成本几乎为零,如果我们丢弃该构建,它所使用的资源可以完全重用。请注意,测试不仅仅是让当前的设计正确,它也是改进设计过程的一部分。复杂系统的硬件工程师经常建立模型(或者,至少,他们用计算机图形可视化设计)。这让他们对设计有了一种“感觉”,但仅仅通过检查设计是不可能得到这种感觉的。软件既不可能也没有必要建立这样的模型。我们只是制造产品本身。即使正式的软件验证可以像编译器一样自动完成,我们仍然会经历构建/测试周期。所以形式验证对于软件行业来说从来没有太大的实际意义。

这就是当前软件开发过程的现实。越来越多的人和组织正在创造更复杂的软件设计。这些设计将用一些编程语言编写,然后通过构建/测试周期进行验证和改进。这个过程容易出错,也不是特别严格。相当多的软件开发人员不愿意相信这就是流程的工作方式,这使得问题变得更加复杂。

目前,大多数软件过程试图将软件设计的不同阶段分成不同的类别。顶层设计完成冻结后才能开始编码。测试和调试只是为了消除构造错误。程序员在中间。他们是软件行业的建筑工人。很多人认为,如果我们能让程序员停止“黑客”行为,按照给他们的设计进行构建(并在过程中少犯错误),那么软件开发就可以变得成熟,成为真正的工程学科。然而,只要该过程忽略工程和经济事实,这种情况就不会发生。

举个例子,任何一个现代工业都无法忍受其制造过程中超过100%的返工率。如果一个建筑工人第一次经常做不好,他很快就会失业。但是在软件行业,即使是最小的一段代码也有可能在测试和调试过程中被修改或完全重写。在一个创造性的过程中(比如设计),我们认识到这种改进不是制造过程的一部分。没有人指望工程师第一次就能创造出完美的设计。即使她做到了,她仍然必须经历改进过程,以证明它是完美的。

即使我们没有从日本的管理方法中学到什么,也要知道,把过程中的错误归咎于工人是不利于提高生产率的。我们不应该总是强迫软件开发遵循不正确的过程模型。相反,我们需要改进过程来帮助而不是阻碍更好软件的生产。这是软件工程的试金石。工程是关于你如何实现过程,而不是关于你是否需要一个CAD系统来产生最终的设计文档。

关于软件开发有一个压倒性的问题,那就是一切都是设计过程的一部分。编码是设计,测试和调试是设计的一部分,我们通常认为的设计仍然是设计的一部分。尽管软件的构建成本很低,但设计成本却高得令人难以置信。软件非常复杂,有许多不同方面的设计内容和设计考虑。问题是所有不同的方面都是相互关联的(就像硬件工程一样)。我们希望顶层设计者可以忽略模块算法设计的细节。同样,我们希望程序员在设计模块内部算法时不必考虑顶层设计问题。不幸的是,一个设计层次的问题会侵入其他层次。对于整个软件系统的成功来说,为特定的模块选择一个算法可能和任何更高层次的设计问题一样重要。在软件设计的不同方面没有重要性的等级。最低模块中的错误设计可能与最高模块中的错误一样致命。软件设计必须是完整的,各方面都是正确的,否则,所有基于这个设计的软件都是错误的。

为了管理复杂性,软件是分层设计的。当一个程序员在考虑一个模块的详细设计时,可能有几百个其他模块,几千个细节,他不可能同时考虑到。比如在软件设计中,有一些重要的方面并不完全属于数据结构和算法的范畴。理想情况下,程序员在设计代码时不应该考虑设计的其他方面。

然而,设计不是这样工作的,原因开始变得清晰。软件设计在编写和测试之前是不完整的。测试是设计验证和改进过程的基本部分。高层结构的设计不是一个完整的软件设计;它只是详细设计的结构框架。我们严格验证高层设计的能力非常有限。详细设计最终对高层次设计的影响至少和其他因素一样大(或者应该允许这种影响)。改进设计的各个方面是一个应该贯穿整个设计周期的过程。如果设计的任何方面被冻结在改进过程之外,那么最终的设计会很糟糕甚至无法工作也就不足为奇了。

如果高级软件设计能成为一个更严谨的工程过程就太好了,但是软件系统的真实情况并不严谨。软件很复杂,它依赖于太多其他东西。也许,一些硬件并不像设计者想的那样工作,或者一个库例程有一个在文档中没有解释的限制。每个软件项目迟早都会遇到这类问题。这几类问题都会在测试的时候发现(如果我们做好测试的话),因为没有办法早期发现。当它们被发现时,它们会强制修改设计。如果我们幸运的话,对设计的改变是局部的。往往变更会影响到整个软件设计的一些重要部分(墨菲定律)。当受影响的设计的一部分由于某种原因无法改变时,那么为了适应这种影响,就必须破坏设计的其他部分。这通常会导致管理者认为的“随机编码”,但这就是软件开发的现实。

例如,在我最近参与的一个项目中,我发现了模块A的内部结构与另一个模块B之间的时间依赖关系,不幸的是,模块A的内部结构隐藏在一个抽象体的后面,该抽象体不允许以任何方式将对模块B的调用组合到其正确的调用序列中。发现问题的时候,当然错过了改变A的抽象体的机会。正如预期的那样,所发生的事情是将越来越多的复杂“修正”应用于A的内部设计。当我们还没有安装完1版本的时候,普遍感觉设计在走下坡路。每一次新的改版都有可能毁掉一些旧的。这是一个正式的软件开发项目。最后,我和同事们决定更改设计,但为了得到管理层的批准,我们不得不自愿加班,而且没有报酬。

在任何大规模的软件项目中,这样的问题都是不可避免的。虽然人们已经用了各种方法来阻止它的出现,但还是忽略了一些重要的细节。这就是技术和工程的区别。如果经验能把我们引向正确的方向,这就是技术。如果经验只会带我们进入未知的领域,那么我们必须使用我们在开始时使用的方法,并通过受控的改进过程使其变得更好,这就是工程。

让我们来看看吧。作为其中很小的一部分,所有程序员都知道,在编码之后而不是之前写软件设计文档,会产生更准确的文档。原因很明显。由代码表示的最终设计是构建/测试周期中唯一改进的东西。在这个周期中,初始设计保持不变的可能性与项目中模块的数量和程序员的数量成反比。很快就会变得一文不值。

在软件工程中,我们需要各个层面的优秀设计。我们尤其需要优秀的顶层设计。初始设计越好,详细设计就越容易。设计师应该使用任何有帮助的东西。结构图、布奇图、状态表、PDL等。-如果有帮助,就用吧。但是,我们必须记住,这些工具和符号不是软件设计。最后,我们必须创建一个真正的软件设计,而且是用编程语言完成的。因此,当我们开始设计时,我们不应该害怕编码。我们必须愿意在必要时改进它们。

到目前为止,还没有一种设计符号可以同时适用于顶层设计和详细设计。设计最终将被表达为用编程语言编写的代码。这意味着在详细设计开始之前,顶层设计符号必须转换成目标编程语言。这一转换步骤需要时间并会引入误差。程序员往往会审核需求并重新设计顶层设计,然后根据自己的实际情况进行编码,而不是从一个可能与所选编程语言不完全映射的符号进行转换。这也是软件开发现实的一部分。

也许,如果设计者自己写初始代码,而不是让别人在后面改造语言无关的设计,效果会更好。我们需要的是一个适合各个层次设计的统一符号。换句话说,我们需要一种编程语言,这种语言也适合捕捉高层次的设计概念。c++正好可以满足这个要求。C++是一种适合真实项目的编程语言,也是一种非常有表现力的软件设计语言。C++允许我们直接表达关于设计组件的高级信息。这样,可以更容易地进行设计,并且在将来可以更容易地改进设计。因为它有更强大的类型检查机制,所以它也有助于检测设计中的错误。这导致更稳健的设计,这实际上是更好的工程设计。

最后,软件设计必须用编程语言来表达,然后通过构建/测试周期来验证和改进。除此之外的任何其他想法都是完全没有用的。请考虑什么软件开发工具和技术是流行的。结构化编程在当时被认为是一种创造性的技术。帕斯卡让它流行起来,自己也因此变得流行起来。面向对象设计是一门新兴的热门技术,C++是其核心。现在,请考虑那些无效的事情。案例工具,流行吗?有;具有普遍性吗?没有。结构图怎么样?情况是一样的。同样,还有华纳-奥尔图、布奇图、对象图以及你能想到的一切。每一个都有自己的优势,只有一个根本的弱点——它不是一个真正的软件设计。事实上,唯一被普遍认可的软件设计符号是PDL,它看起来像什么?

这说明* * *在软件行业的重要性是压倒一切的,比起潜意识里知道编程技术,尤其是实际开发中使用的编程语言的提高,在软件行业比什么都重要。也说明程序员是在乎设计的。当更具表现力的编程语言出现时,软件开发人员会使用它们。

同样,请考虑软件开发过程是如何变化的。从前,我们使用瀑布过程。现在,我们讨论的是螺旋开发和快速原型。虽然这些技术经常被认为是“消除风险”和“缩短产品的交付时间”,但它们实际上只是为了在软件生命周期中更早地开始编码。这是好事。这允许构建/测试周期更早地开始验证和改进设计。这也意味着顶级软件设计师很可能会进行详细设计。

如上所述,工程更多的是关于如何实现过程,而不是最终产品是什么样子。在软件行业,我们已经接近工程师的标准,但是需要一些认知上的改变。编程和构建/测试周期是工程软件过程的中心。我们需要以这种方式管理它们。构建/测试周期的经济规律,加上软件系统几乎可以代表任何东西的事实,使得我们完全不可能找到一种通用的方法来验证软件设计。我们可以改进这个过程,但不能脱离它。

最后一点:任何工程设计项目的目标都是一些文档产品。显然,实际设计的文档是最重要的,但它们不是唯一要制作的文档。最终,有人会使用这个软件。同样,该系统很可能需要后续的修改和增强。这意味着辅助文档对于软件项目和硬件项目一样重要。虽然暂时忽略了用户手册、安装指南等与设计过程没有直接关系的文档,但是仍然有两个重要的需求需要利用辅助设计文档来解决。

辅助文档的第一个目的是从问题空间捕捉重要信息,这些信息不能直接用于设计。软件设计需要创建一些软件概念来对问题空间中的概念进行建模。这个过程要求我们理解问题空间中的概念。通常,这种理解会包含一些不会在软件空间中直接建模的信息,但是这些信息仍然会帮助设计者确定什么是基本概念以及如何最好地对它们建模。这些信息应该记录在某个地方,以防您以后想要更改模型。

辅助文档的第二个重要需求是记录设计的某些方面,这些方面很难直接从设计本身提取。它们可以是高级内容,也可以是低级内容。对于其中的许多方面,图形是描述它们的最佳方式。这使得它们很难作为注释包含在代码中。这并不是说应该使用图形化的软件设计符号来代替编程语言。这和用一些文字描述来补充硬件科目的平面设计文档没什么区别。

永远不要忘记,决定实际设计真实面貌的是源代码,而不是辅助文档。理想情况下,可以使用软件工具对源代码进行后处理,生成辅助文档。我们可能对此期望过高。接下来,程序员(或技术作者)可以使用一些工具从源代码中提取一些特定的信息,然后他们可以用其他方式记录这些信息。毫无疑问,手动更新这种文档是很困难的。这是支持需要更具表现力的编程语言的另一个原因。同样,这也是支持将这个辅助文档保持在最低限度,并在项目中尽可能晚地将其正式化的一个原因。同样,我们可以使用一些好的工具;否则,我们不得不求助于铅笔、纸和黑板。

总结如下:

实际的软件在计算机中运行。它是存储在某种磁介质中的0和1的序列。它不是用C++(或任何其他编程语言)编写的程序。

程序列表是代表软件设计的文档。事实上,是编译器和连接器构建了软件设计。

令人难以置信的是,构建一个实际的软件设计是多么便宜,而且随着计算机速度的加快,它总是变得更便宜。

设计实际软件的成本令人难以置信,因为软件的复杂程度令人难以置信,软件项目的几乎所有步骤都是设计过程的一部分。

编程是一种设计活动——一个好的软件设计过程认识到了这一点,当编码有意义时,就会毫不犹豫地编码。

编码比我们想象的更频繁地显示出它的意义。通常,用代码表达设计的过程会暴露出一些遗漏和额外的设计需求。这种情况越早发生,设计就越好。

因为软件构建起来非常便宜,所以正式的工程验证方法在实际的软件开发中用处不大。仅仅构建一个设计并测试它比试图证明它更简单也更便宜。

测试和调试是设计活动——对于软件来说,它们相当于其他工程学科中的设计验证和改进过程。一个好的软件设计过程认识到了这一点,并且不会试图减少这些步骤。

还有其他的设计活动——称之为高层设计、模块设计、结构设计、架构设计之类的。一个好的软件设计过程认识到这一点,并仔细包括这些步骤。

所有的设计活动都是互动的。当不同的设计步骤显示出必要性时,一个好的软件设计过程认识到这一点,并允许设计变更,有时甚至是根本性的变更。

许多不同的软件设计符号可能是有用的——它们可以作为辅助文档和工具来帮助简化设计过程。它们不是软件设计。

软件开发仍然是一门手艺,而不是一门工程学科。主要是在验证和改进设计的关键过程中不够严谨。

最后,软件开发的真正进步取决于编程技术的进步,而编程技术的进步又意味着编程语言的进步。C++就是这样一个改进。它获得了爆炸性的流行,因为它是一种直接支持更好的软件设计的主流编程语言。

C++已经朝着正确的方向迈出了一步,但还需要更多的进步。