什么是软件设计

      至今,我仍能记起当我顿悟并最终产生下面文章时所在的地方。那是1986年的夏天,我在加利福尼亚中国湖海军武器中心担任临时顾问。在这期间,我有 幸参加了一个关于Ada的研讨会。讨论当中,有一位听众提出了一个具有代表性的问题,“软件开发者是工程师吗?”我不记得当时的回答,但是我却记得当时并 没有真正解答这个问题。于是,我就退出讨论,开始思考我会怎样回答这样一个问题。现在,我无法肯定当时我为什么会记起几乎10年前曾经在 Datamation杂志上阅读过的一篇论文,不过促使我记起的应该是后续讨论中的某些东西。这篇论文阐述了工程师为什么必须是好的作家(我记得该论文谈 论就是这个问题——好久没有看了),但是我从该论文中得到的关键一点是:作者认为工程过程的最终结果是文档。换句话说,工程师生产的是文档,不是实物。其 他人根据这些文档去制造实物。于是,我就在困惑中提出了一个问题,“除了软件项目正常产生的所有文档以外,还有可以被认为是真正的工程文档的东西吗?”我 给出的回答是,“是的,有这样的文档存在,并且只有一份——源代码。”

把源代码看作是一份工程文档——设计 ——完全颠覆了我对自己所选择的职业的看法。它改变了我看待一切事情的方式。此外,我对它思考的越多,我就越觉得它阐明了软件项目常常遇到的众多问题。更 确切地说,我觉得大多数人不理解这个不同的看法,或者有意拒绝它这样一个事实,就足以说明很多问题。几年后,我终于有机会把我的观点公开发表。C++ Journal中的一篇有关软件设计的论文促使我给编辑写了一封关于这个主题的信。经过几封书信交换后,编辑Livleen Singh同意把我关于这个主题的想法发表为一篇论文。下面就是这篇文章。

什么是软件设计?

面 向对象技术,特别是C++,似乎给软件界带来了不小的震动。出现了大量的论文和书籍去描述如何应用这项新技术。总的来说,那些关于面向对象技术是否只是一 个骗局的问题已经被那些关于如何付出最小的努力即可获得收益的问题所替代。面向对象技术出现已经有一段时间了,但是这种爆炸式的流行却似乎有点不寻常。人 们为何会突然关注它呢?对于这个问题,人们给出了各种各样的解释。事实上,很可能就没有单一的原因。也许,把多种因素的结合起来才能最终取得突破,并且这 项工作正在进展之中。尽管如此,在软件革命的这个最新阶段中,C++本身看起来似乎成为了一个主要因素。同样,对于这个问题,很可能也存在很多种理由,不 过我想从一个稍微不同的视角给出一个答案:C++之所以变得流行,是因为它使软件设计变得更容易的同时,也使编程变得更容易。

虽 然这个解释好像有点奇特,但是它却是深思熟虑的结果。在这篇论文中,我就是想要关注一下编程和程序设计之间的关系。近10年来,我一直觉得整个软件行业都 没有觉察到做出一个软件设计和什么是真正的软件设计之间的一个微妙的不同点。只要看到了这一点,我认为我们就可以从C++增长的流行趋势中,学到关于如何 才能成为更好的软件工程师的意义深远的知识。这个知识就是,编程不是构建软件,而是设计软件。

几年前,我参见 了一个讨论会,其中讨论到软件开发是否是一门工程学科的问题。虽然我不记得了讨论结果,但是我却记得它是如何促使我认识到:软件业已经做出了一些错误的和 硬件工程的比较,而忽视了一些绝对正确的对比。其实,我认为我们不是软件工程师,因为我们没有认识到什么才是真正的软件设计。现在,我对这一点更是确信无 疑。

任何工程活动的最终目标都是某些类型的文档。当设计工作完成时,设计文档就被转交给制造团队。该团队是一 个和设计团队完全不同的群体,并且其技能也和设计团队完全不同。如果设计文档正确地描绘了一个完整的设计,那么制造团队就可以着手构建产品。事实上,他们 可以着手构建该产品的许多实物,完全无需设计者的任何进一步的介入。在按照我的理解方式审查了软件开发的生命周期后,我得出一个结论:实际上满足工程设计 标准的惟一软件文档,就是源代码清单。

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

在把代码看作是软件设计所带来的结果中,有一个结 果完全盖过了所有其他的结果。它非常重要并且非常明显,也正因为如此,对于大多数软件机构来说,它完全是一个盲点。这个结果就是:软件的构建是廉价的。它 根本就不具有昂贵的资格;它非常的廉价,几乎就是免费的。如果源代码是软件设计,那么实际的软件构建就是由编译器和连接器完成的。我们常常把编译和连接一 个完整的软件系统的过程称为“进行一次构建”。在软件构建设备上所进行的主要投资是很少的——实际需要的只有一台计算机、一个编辑器、一个编译器以及一个 连接器。一旦具有了一个构建环境,那么实际的软件构建只需花费少许的时间。编译50 000行的C++程序也许会花费很长的时间,但是构建一个具有和50 000行C++程序同样设计复杂性的硬件系统要花费多长的时间呢?

把 源代码看作是软件设计的另外一个结果是,软件设计相对易于创作,至少在机械意义上如此。通常,编写(也就是设计)一个具有代表性的软件模块(50至100 行代码)只需花费几天的时间(对它进行完全的调试是另外一个议题,稍后会对它进行更多的讨论)。我很想问一下,是否还有任何其他的学科可以在如此短的时间 内,产生出和软件具有同样复杂性的设计来,不过,首先我们必须要弄清出如何来度量和比较复杂性。然而,有一点是明显的,那就是软件设计可以 极为迅速地变得非常庞大。

假设软件设计相对易于创作,并且在本质上构建起来也 没有什么代价,一个不令人吃惊的发现是,软件设计往往是难以置信的庞大和复杂。这看起来似乎很明显,但是问题的重要性却常常被忽视。学校中的项目通常具有 数千行的代码。具有10 000行代码(设计)的软件产品被它们的设计者丢弃的情况也是有的。我们早就不再关注于简单的软件。典型的商业软件的设计都是由数十万行代码组成的。许多 软件设计达到了上百万行代码。另外,软件设计几乎总是在不断地演化。虽然当前的设计可能只有几千行代码,但是在产品的生命期中,实际上可能要编写许多倍的 代码。

尽 管确实存在一些硬件设计,它们看起来似乎和软件设计一样复杂,但是请注意两个有关现代硬件的事实。第一,复杂的硬件工程成果未必总是没有错误的,在这一点 上,它不存在像软件那样让我们相信的评判标准。多数的微处理器在发售时都具有一些逻辑错误:桥梁坍塌,大坝破裂,飞机失事以及数以千计的汽车和其他消费品 被召回——所有的这些我们都记忆犹新,所有的这些都是设计错误的结果。第二,复杂的硬件设计具有与之对应的复杂、昂贵的构建阶段。结果,制造这种系统所需 的能力限制了真正能够生产复杂硬件设计公司的数目。对于软件来说,没有这种限制。目前,已经有数以百计的软件机构和数以千计的非常复杂的软件系统存在,并 且数量以及复杂性每天都在增长。这意味着软件行业不可能通过仿效硬件开发者找到针对自身问题的解决办法。倘若一定要说出有什么相同之处的话,那就是,当 CAD和CAM可以做到帮助硬件设计者创建越来越复杂的设计时,硬件工程才会变得和软件开发越来越像。

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

一般的看法认为,当真正的工程师完成了一个设计,不管该设计有多么复 杂,他们都非常确信该设计是可以工作的。他们也非常确信该设计可以使用公认的技术建造出来。为了做到这一点,硬件工程师花费了大量的时间去验证和改进他们 的设计。例如,请考虑一个桥梁设计。在这样一个设计实际建造之前,工程师会进行结构分析——他们建立计算机模型并进行仿真,他们建立比例模型并在风洞中或 者用其他一些方法进行测试。简而言之,在建造前,设计者会使用他们能够想到的一切方法来证实设计是正确的。对于一架新型客机的设计来说,情况甚至更加严 重;必须要构建出和原物同尺寸的原型,并且必须要进行飞行测试来验证设计中的种种预计。

对于大多数人来说,软 件中明显不存在和硬件设计同样严格的工程。然而,如果我们把源代码看做是设计,那么就会发现软件工程师实际上对他们的设计做了大量的验证和改进。软件工程 师不把这称为工程,而称它为测试和调试。大多数人不把测试和调试看作是真正的“工程”——在软件行业中肯定没有被看作是。造成这种看法的原因,更多的是因 为软件行业拒绝把代码看作设计,而不是任何实际的工程差别。事实上,试验模型、原型以及电路试验板已经成为其他工程学科公认的组成部分。软件设计者之所以 不具有或者没有使用更多的正规方法来验证他们的设计,是因为软件构建周期的简单经济规律。

第一个启示:仅仅构 建设计并测试它比做任何其他事情要廉价一些,也简单一些。我们不关心做了多少次构建——这些构建在时间方面的代价几乎为零,并且如果我们丢弃了构建,那么 它所使用的资源完全可以重新利用。请注意,测试并非仅仅是让当前的设计正确,它也是改进设计的过程的一部分。复杂系统的硬件工程师常常建立模型(或者,至 少他们把设计用计算机图形直观地表现出来)。这就使得他们获得了对于设计的一种“感觉”,而仅仅去检查设计是不可能获得这种感觉的。对于软件来说,构建这 样一个模型既不可能也无必要。我们仅仅构建产品本身。即使正规的软件验证可以和编译器一样自动进行,我们还是会去进行构建/测试循环。因此,正规的验证对 于软件行业来说从来没有太多的实际意义。

这就是现今软件开发过程的现实。数量不断增长的人和机构正在创建着更 加复杂的软件设计。这些设计会被先用某些编程语言编写出来,然后通过构建/测试循环进行验证和改进。过程易于出错,并且不是特别的严格。相当多的软件开发 人员并不想相信这就是过程的运作方式,也正因为这一点,使问题变得更加复杂。
当前大多数的软件过程都试图把软件设计的不同阶段分离到不同的类别中。必须要在顶层的设计完成并且冻结后,才能开始编码。测试和调试只对清除建造错误是必 要的。程序员处在中间位置,他们是软件行业的建造工人。许多人认为,如果我们可以让程序员不再进行“随意的编码(hacking)”并且按照交给他们的设 计去进行构建(还要在过程中,犯更少的错误),那么软件开发就可以变得成熟,从而成为一门真正的工程学科。但是,只要过程忽视了工程和经济学事实,这就不 可能发生。

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

即 使我们从日本的管理方法中没有学到任何东西,我们也应该知道由于在过程中犯错误而去责备工人是无益于提高生产率的。我们不应该不断地强迫软件开发去符合不 正确的过程模型,相反,我们需要去改进过程,使之有助于而不是阻碍产生更好的软件。这就是“软件工程”的石蕊测试。工程是关于你如何实施过程的,而不是关 于是否需要一个CAD系统来产生最终的设计文档。

关于软件开发有一个压倒性的问题,那就是一切都是设计过程的 一部分。编码是设计,测试和调试是设计的一部分,并且我们通常认为的设计仍然是设计的一部分。虽然软件构建起来很廉价,但是设计起来却是难以置信的昂贵。 软件非常的复杂,具有众多不同方面的设计内容以及它们所导致的设计考虑。问题在于,所有不同方面的内容是相互关连的(就像硬件工程中的一样)。我们希望顶 层设计者可以忽视模块算法设计的细节。同样,我们希望程序员在设计模块内部算法时不必考虑顶层设计问题。糟糕的是,一个设计层面中的问题侵入到了其他层面 之中。对于整个软件系统的成功来说,为一个特定模块选择算法可能和任何一个更高层次的设计问题同样重要。在软件设计的不同方面内容中,不存在重要性的等 级。最低层模块中的一个不正确设计可能和最高层中的错误一样致命。软件设计必须在所有的方面都是完整和正确的,否则,构建于该设计基础之上的所有软件都会 是错误的。

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

但是,设计并不是以这种方式工作的,并且原因也开始变得明朗。 软件设计只有在其被编写和测试后才算完成。测试是设计验证和改进过程的基础部分。高层结构的设计不是完整的软件设计;它只是细节设计的一个结构框架。在严 格地验证高层设计方面,我们的能力是非常有限的。详细设计最终会对高层设计造成的影响至少和其他的因素一样多(或者应该允许这种影响)。对设计的各个方面 进行改进,是一个应该贯穿整个设计周期的过程。如果设计的任何一个方面内容被冻结在改进过程之外,那么对于最终设计将会是糟糕的或者甚至无法工作这一点, 就不会觉得奇怪了。

如果高层的软件设计可以成为一个更加严格的工程过程,那该有多好呀,但是软件系统的真实情 况不是严格的。软件非常的复杂,它依赖于太多的其他东西。或许,某些硬件没有按照设计者认为的那样工作,或者一个库例程具有一个文档中没有说明的限制。每 一个软件项目迟早都会遇到这些种类的问题。这些种类的问题会在测试期间被发现(如果我们的测试工作做得好的话),之所以如此是因为没有办法在早期就发现它 们。当它们被发现时,就迫使对设计进行更改。如果我们幸运,那么对设计的更改是局部的。时常,更改会波及到整个软件设计中的一些重要部分(莫非定律)。当 受到影响的设计的一部分由于某种原因不能更改时,那么为了能够适应影响,设计的其他部分就必须得遭到破坏。这通常导致的结果就是管理者所认为的“随意编 码”,但是这就是软件开发的现实。

例如,在我最近工作的一个项目中,发现了模块A的内部结构和另一个模块B之 间的一个时序依赖关系。糟糕的是,模块A的内部结构隐藏在一个抽象体的后面,而该抽象体不允许以任何方法把对模块B的调用合入到它的正确调用序列中。当问 题被发现时,当然已经错过了更改A的抽象体的时机。正如所料,所发生的就是把一个日益增长的复杂的“修正”集应用到A的内部设计上。在我们还没有安装完版 本1时,就普遍感觉到设计正在衰退。每一个新的修正很可能都会破坏一些老的修正。这是一个正规的软件开发项目。最后,我和我的同事决定对设计进行更改,但 是为了得到管理层的同意,我们不得不自愿无偿加班。

在任何一般规模的软件项目中,肯定会出现像这样的问题,尽 管人们使用了各种方法来防止它的出现,但是仍然会忽视一些重要的细节。这就是工艺和工程之间的区别。如果经验可以把我们引向正确的方向,这就是工艺。如果 经验只会把我们带入未知的领域,然后我们必须使用一开始所使用的方法并通过一个受控的改进过程把它变得更好,这就是工程。

我 们来看一下只是作为其中很小一点的内容,所有的程序员都知道,在编码之后而不是之前编写软件设计文档会产生更加准确的文档。现在,原因是显而易见的。用代 码来表现的最终设计是惟一一个在构建/测试循环期间被改进的东西。在这个循环期间,初始设计保持不变的可能性和模块的数量以及项目中程序员的数量成反比。 它很快就会变得毫无价值。

在软件工程中,我们非常需要在各个层次都优秀的设计。我们特别需要优秀的顶层设计。 初期的设计越好,详细设计就会越容易。设计者应该使用任何可以提供帮助的东西。结构图表、Booch 图、状态表、PDL等等——如果它能够提供帮助,就去使用它。但是,我们必须记住,这些工具和符号都不是软件设计。最后,我们必须创建真正的软件设计,并 且是使用某种编程语言完成的。因此,当我们得出设计时,我们不应该害怕对它们进行编码。在必要时,我们必须应该乐于去改进它们。

至 今,还没有任何设计符号可以同时适用于顶层设计和详细设计。设计最终会表现为以某种编程语言编写的代码。这意味着在详细设计可以开始前,顶层设计符号必须 被转换成目标编程语言。这个转换步骤耗费时间并且会引入错误。程序员常常是对需求进行回顾并且重新进行顶层设计,然后根据它们的实际去进行编码,而不是从 一个可能没有和所选择的编程语言完全映射的符号进行转换。这同样也是软件开发的部分现实情况。

也许,如果让设 计者本人来编写初始代码,而不是后来让其他人去转换语言无关的设计,就会更好一些。我们所需要的是一个适用于各个层次设计的统一符号。换句话说,我们需要 一种编程语言,它同样也适用于捕获高层的设计概念。C++正好可以满足这个要求。C++是一门适用于真实项目的编程语言,同时它也是一个非常具有表达力的 软件设计语言。C++允许我们直接表达关于设计组件的高层信息。这样,就可以更容易地进行设计,并且以后可以更容易地改进设计。由于它具有更强大的类型检 查机制,所以也有助于检测到设计中的错误。这就产生了一个更加健壮的设计,实际上也是一个更好的工程化设计。

最 后,软件设计必须要用某种编程语言表现出来,然后通过一个构建/测试循环对其进行验证和改进。除此之外的任何其他主张都完全没有用。请考虑一下都有哪些软 件开发工具和技术得以流行。结构化编程在它的时代被认为是创造性的技术。Pascal使之变得流行,从而自己也变得流行。面向对象设计是新的流行技术,而 C++是它的核心。现在,请考虑一下那些没有成效的东西。CASE工具,流行吗?是的;通用吗?不是。结构图表怎么样?情况也一样。同样地,还有 Warner-Orr图、Booch图、对象图以及你能想起的一切。每一个都有自己的强项,以及惟一的一个根本弱点——它不是真正的软件设计。事实上,惟 一一个可以被普遍认可的软件设计符号是PDL,而它看起来像什么呢?

这表明,在软件业的共同潜意识中本能地知道,编程技术,特别是实际开发所使用的编程语言的改进和软件行业中任何其他东西相比,具有压倒性的重要性。这还表明,程序员关心的是设计。当出现更加具有表达力的编程语言时,软件开发者就会使用它们。

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

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

最后一点:任何工程设计项目 的目标是一些文档产品。显然,实际设计的文档是最重要的,但是它们并非惟一要产生的文档。最终,会期望某些人来使用软件。同样,系统很可能也需要后续的修 改和增强。这意味着,和硬件项目一样,辅助文档对于软件项目具有同样的重要性。虽然暂时忽略了用户手册、安装指南以及其他一些和设计过程没有直接联系的文 档,但是仍然有两个重要的需求需要使用辅助设计文档来解决。

辅助文档的第一个用途是从问题空间中捕获重要的信 息,这些信息是不能直接在设计中使用的。软件设计需要创造一些软件概念来对问题空间中的概念进行建模。这个过程需要我们得出一个对问题空间中概念的理解。 通常,这个理解中会包含一些最后不会被直接建模到软件空间中的信息,但是这些信息却仍然有助于设计者确定什么是本质概念以及如何最好地对它们建模。这些信 息应该被记录在某处,以防以后要去更改模型。

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

决不要忘记,是源代码决定了实际设计的真实样子,而不是辅助文档。在理想情况下,可以使用软件 工具对源代码进行后期处理并产生出辅助文档。对于这一点,我们可能期望过高了。次一点的情况是,程序员(或者技术方面的编写者)可以使用一些工具从源代码 中提取出一些特定的信息,然后可以把这些信息以其他一些方式文档化。毫无疑问,手工对这种文档保持更新是困难的。这是另外一个支持需要更具表达力的编程语 言的理由。同样,这也是一个支持使这种辅助文档保持最小并且尽可能在项目晚期才使之变成正式的理由。同样,我们可以使用一些好的工具;不然的话,我们就得 求助于铅笔、纸以及黑板。

总结如下:

  • 实际的软件运行于计算机之中。它是存储在某种磁介质中的0和1的序列。它不是使用C++语言(或者其他任何编程语言)编写的程序。
  • 程序清单是代表软件设计的文档。实际上把软件设计构建出来的是编译器和连接器。
  • 构建实际软件设计的廉价程度是令人难以置信的,并且它始终随着计算机速度的加快而变得更加廉价。
  • 设计实际软件的昂贵程度是令人难以置信的,之所以如此,是因为软件的复杂性是令人难以置信的,并且软件项目的几乎所有步骤都是设计过程的一部分。
  • 编程是一种设计活动——好的软件设计过程认可这一点,并且在编码显得有意义时,就会毫不犹豫的去编码。
  • 编码要比我们所认为的更频繁地显现出它的意义。通常,在代码中表现设计的过程会揭示出一些疏漏以及额外的设计需要。这发生的越早,设计就会越好。
  • 因为软件构建起来非常廉价,所以正规的工程验证方法在实际的软件开发中没有多大用处。仅仅建造设计并测试它要比试图去证明它更简单、更廉价。
  • 测试和调试是设计活动——对于软件来说,它们就相当于其他工程学科中的设计验证和改进过程。好的软件设计过程认可这一点,并且不会试图去减少这些步骤。
  • 还有一些其他的设计活动——称它们为高层设计、模块设计、结构设计、构架设计或者诸如此类的东西。好的软件设计过程认可这一点,并且慎重地包含这些步骤。
  • 所有的设计活动都是相互影响的。好的软件设计过程认可这一点,并且当不同的设计步骤显示出有必要时,它会允许设计改变,有时甚至是根本上的改变,
  • 许多不同的软件设计符号可能是有用的——它们可以作为辅助文档以及工具来帮助简化设计过程。它们不是软件设计。
  • 软件开发仍然还是一门工艺,而不是一个工程学科。主要是因为缺乏验证和改善设计的关键过程中所需的严格性。
  • 最后,软件开发的真正进步依赖于编程技术的进步,而这又意味着编程语言的进步。C++就是这样的一个进步。它已经取得了爆炸式的流行,因为它是一门直接支持更好的软件设计的主流编程语言。
  • C++在正确的方向上迈出了一步,但是还需要更大的进步。

后 记

当 我回顾几乎10年前所写的东西时,有几点让我印象深刻。第一点(也是和本书最有关的)是,现今,我甚至比那时更加确信我试图去阐述的要点在本质上的正确 性。随后的一些年中,许多流行的软件开发方法增强了其中的许多观点,这支持了我的信念。最明显的(或许也是最不重要的)是面向对象编程语言的流行。现在, 除了C++外,出现了许多其他的面向对象编程语言。另外,还有一些面向对象设计符号,比如:UML。我关于面向对象语言之所以得到流行是因为它们允许在代 码中直接表现出更具表达力的设计的论点,现在看来有点过时了。

重构的概念——重新组织代码基础,使之更加健壮和可重用——同样也和我的关于设计的所有方面的内容都应该是灵活的并且在验证设计时允许改变的论点相似。重构只是提供了一个过程以及一组如何去改善已经被证实具有缺陷的设计的准则。

最后,文中有一个敏捷开发的总的概念。虽然极限编程是这些新方法中最知名的一个,但是它们都具有一个共同点:它们都承认源代码是软件开发工作中的最重要的产品。

另 一方面,有一些观点——其中的一些我在论文中略微谈到过——在随后的一些年中,对我来说变得更加重要。第一个是构架,或者顶层设计的重要性。在论文中,我 认为构架只是设计的一部分内容,并且在构建/测试循环对设计进行验证的过程中,构架需要保持可变。这在本质上是正确的,但是回想起来,我认为我的想法有点 不成熟。虽然构建/测试循环可能揭示出构架中的问题,但是更多的问题是常常由于改变需求而表现出来的。 一般来说,设计软件是困难的,并且新的编程语言,比如:Java或者C++,以及图形化的符号,比如:UML,对于不知道如何有效地使用它的人来说,都没 有多大的帮助。此外,一旦一个项目基于一个构架构建了大量的代码,那么对该构架进行基础性的更改,常常相当于丢弃掉该项目并重新开始一个,这就意味着该项 目没有出现过。即使项目和机构在根本上接受了重构的概念,但是他们通常仍然不愿意去做一些看起来就像是完全重写的事情。这意味着第一次就把它作对(或者至 少是接近对)是重要的,并且项目变得越大,就越要如此。幸运的是,软件设计模式有助于解决这方面问题。

还 有其他一些方面的内容,我认为需要更多地强调一下,其中之一就是辅助文档,尤其是构架方面的文档。虽然源代码就是设计,但是试图从源代码中得出构架,可能 是一个令人畏惧的体验。在论文中,我希望能够出现一些软件工具来帮助软件开发者自动地维护来自源代码的辅助文档。我几乎已经放弃了这个希望。一个好的面向 对象构架通常可以使用几幅图以及少许的十几页文本描述出来。不过,这些图(和文本)必须集中于设计中的关键类和关系。糟糕的是,对于软件设计工具可能会变 得足够聪明,以至于可以从源代码的大量细节中提取Jack Reeves出这些重要方面的内容这一点,我没有看到任何真正的希望。这意味着还得必须由人来编写和维护这种文档。我仍然认为,在源代码完成后,或者至少 是在编写源代码的同时去编文档,要比在编写源代码之前去编写文档更好一些。

最后,我在论文的最后谈到了C++是编程——并且因此是软件设计——艺术的一个进步,但是还需要更大的进步。就算我完全没有看到语言中出现任何真正的编程进步来挑战C++的流行,那么在今天,我会认为这一点甚至要比我首次编写它时更加正确。

软件设计方案 用户界面设计规范 用户界面:又称人机界面,实现用户与计算机之间的通信,以控制计算机或进行用户与计算机之间的数据传送的系统部件。 GUI:即图形用户界面,一种可视化的用户界面,它使用图形界面代替正文界面。 本系统坚持图形用户界面(GUI)设计原则,界面直观、对用户透明。用户接触软件后对界面上对应的功能一目了然、不需要多少培训就可以方便地使用本应用系统。 1、界面设计介绍 界面设计是为了满足软件专业化标准化的需求而产生的对软件的使用界面进行美化优化规范化的设计分支。 1)软件启动封面设计 应使软件启动封面最终为高清晰度的图像,选用的色彩不宜超过256色,大小多为主流显示器分辨率的1/6大。启动封面上应该醒目地标注制作或支持的公司标志、产品商标、软件名称、版本号、网址、版权声明、序列号等信息,以树立软件形象,方便使用者或购买者在软件启动的时候得到提示。插图宜使用具有独立版权的、象征性强的、识别性高的、视觉传达效果好的图形,若使用摄影也应该进行数位处理,以形成该软件的个性化特征。如果是系列软件还将考虑整体设计的统一和延续性。 2)软件框架设计 软件的框架设计要复杂得多。软件框架设计应该简洁明快,尽量少用无谓的装饰,应该考虑节省屏幕空间,各种分辨率的大小,缩放时的状态和原则,并且为将来设计的按钮、菜单、标签、滚动条及状态栏预留位置。设计中将整体色彩组合进行合理搭配,将软件商标放在显著位置,主菜单应放在左边或上边,滚动条放在右边,状态栏放在下边,以符合视觉流程和用户使用心理。 3)软件按钮设计 软件按钮设计应该具有交互性,即应该有3到6种状态效果:点击前鼠标未放在上面时的状态;鼠标放在上面但未点击的状态;点击时状态;点击后鼠标未放在上面时的状态;不能点击时状态;独立自动变化的状态。按钮应具备简洁的图示效果,名称易懂,用词准确,能望文知意最好,让使用者产生功能关联反应,群组内按钮应该风格统一,功能差异大的按钮应该有所区别。
范围:CPU上可以识别的代码和数据。全部的代码总和。 要求:从定义开始的设计。完整性,彻底地定义从无开始的整个设计。这是因为软件之软,也是因为硬件平台的多样性和特殊性。 完整把握,从头设计是第一原则。因为软件世界自己并不能统一,还有继续分化的趋势。没有根本一致的基础可能是软件的本性。退回到一无所有是处理软件问题的根本。 在这样的视野下,操作系统只是一个部分,一个模块,不同的操作系统任你选择;语言的选择是运行环境的选择(每种语言有每种语言的运行时布局);所谓框架只是“类库+运行环境”的一种构造。 没有对其负载能力、操作强度进行评估前,这些东西(操作系统、语言、框架)还都不能纳入设计规范。 性能:运行过程的收敛(长时间运行的均态)。操作强度设计(串行处理速度),负载能力设计(并发处理的量)。可靠性设计。 软件问题的3个方面: 1、硬件,软件的操作对象和运行软件的数字系统(CPU系统和数字加速硬件) 2、交互操作(界面),专业界面设计 3、软件调度性能,实时的自动化过程(设备控制和自动测量)和用户交互过程(请求服务过程和干预过程;本地交互和远程交互),程控和网络访问的调度(服务器)。 软件项目的3个部分:(把3个阶段由纵向横过来,进行统筹) 分解文档,集成平台,可维护性要求。 软件设计必须有自说明特性。不能对文档产生依赖性。软件代码中合适的地方,需要对文档进行恰如其分说明。原则是,每段代码,每处需要理解的地方,如果和总体架构相关,就要有说明。 软件领域需要简化。需要还原软件本来的面目。EDA有泛滥的趋势,软件的各个方面都需要简化。软件形态、需求分析、文档说明、开发工具等。 需求分析过分强调适应生命周期的变化和没有需求分析是一样的。不切实际的面向未来的需求架构的直接结果是软件的复杂和错误百出。 软件只有一个,而观察的视角很多。要采用最适合的观察视角,让软件一目了然。 软件的生成过程和观察过程是两个不同的观念。生成过程又可以区分为:研究过程和工程过程。研究过程可以通过结果,研究报告反映;工程过程则必须采用过程刻画。 软件规范使用的语言一定要有普遍语义,但描述本身具有特殊性;不能强求它的全球唯一。一定要雄视全体,才能选择正确的立足点,这就要求对目前的软件技术有一个了解;要考虑纳入新的发展,那么规范应该分层,把一般的和具体易变的成分分开;要有具体的指导意义,越具体指导意义越大,但通用性则越小。 所谓架构,可能是十分具体应用的代表;不同类别的应用必然有不同的架构。软件架构本身是“应用架构”。因此,不能规范具体的架构。到是可以做:应用架构规范的规范。 逻辑架构的特殊性。可以判断,任何一款实用的软件采取的软件逻辑抽象都是别样的,特例的逻辑。否则,软件不可能那么轻快实用。软件逻辑,鬼魅也。而需求分析,必须是现实实用的,而不是同构/仿真的-这似乎是反对象分析的。因为这里强调的是和软件的交互界面,这个界面远远没有反映现实世界的结构。须知,软件强调的是数据处理,是输入输出。否则,就不能达到最简化。 可能现实世界的结构映射,最适合的方式是数据库 - 采用纯数据结构进行映射。除此之外,能有更合适的技术吗? 面向对象建模是吗?那么对象又如何与现实世界的对象绑定在一起呢? 这再次表明,在软件技术和需求分析之间有鸿沟。软件技术作为特殊的技术,有它的有限性。也反映了,包含软件应用在内的现实架构已经固定。 如果软件是数据处理,是输入输出,那么软件结构也就可以确定了! 可视化、用户操作界面解开了另外的软件世界,因为可视化可以代表用户更抽象的逻辑。用户希望操作可视对象,象操作现实对象一样。软件从模拟现实对象的过程中继承了其结构。 工业控制也开启了新的软件世界,因为软件要从分离的输入建立“综合感知”,感知到设备状态,然后做出响应。 软件有其固有的物理属性,也就是计算的量。算法领域,无论算法的论证多么曲折,求得的结果,物化为软件,总是“早已熟知”的软件。这一区分,是定义软件规范的基石。 算法构造领域是和软件完全不同的领域,算法不是软件。算法类似数学系统。也一如数学系统那样多样。 软件构造。算法总要转化为软件,这就是软件构造问题。寻址系统,数组。软件把自己的生成作为问题,给算法开辟了新的领域。软件生成,是一个“构造-编译”问题。手工构造,自动编译。语言的发展,是一个软件生成的历史。所谓统一建模,所谓设计模式,其实都是软件生成的问题。 需求分析。需求分析本质上是独立的。所谓OOA,面向对象的建模,把程序构造概念上升到需求分析领域可能是不对的。一个先验的,复杂的难于掌握的限制,只会让人对需求分析望而却步;即使勉强掌握,难求对需求分析的创造性发展。需求分析应该专注于需求分析本身,独立发展,一切为了准确、快捷的分析。 需求分析层次高一些,抽象一些,自由一些,这样可以充分表达需求的本质。反而可以促进更高级别的程序自动生成。 软件生成的历史。软件生成是为了解决人机沟通,让“计算机语言”更接近普通人的思维逻辑。把这种“高级计算机语言”翻译成可以执行的代码,就是软件生成(代码生成)的任务。而软件编制是专业人员的事情,因此语言问题的本质其实不那么重要。须知,经过培训,莫尔司码的电报发报可以比说话的语速还快!因此,计算机语言的前途迷茫;实际上也确实迷茫,历史上语言的层出不穷本身就说明了问题,至今仍然如此。在当今,必须建立这样的观点:语言是因人而异的;面对一个语言的时候,要清醒,首先明确“这是为谁设计的语言”;也就是说,需求分析之前的需求是要明确,让什么人来设计软件,然后为他们选择合适的语言软件生成除了代码生成,还包括另外一个意思:软件构造。这在前面已经论述过了。只是,这里的软件构造机制已经在语言中奠定了。手工参与的软件构造只是语言给出的构造机制的应用。手工的软件构造就是语言构造机制的复制,产生大量的代码,应付实际问题的量。 立体构造。这里还有一个立体问题,实际问题的构造可能产生立体构造,如同建筑,基本的构件组装出复杂的立体结构。这里是建筑设计师的功劳。可能目前我们在语言层面上混淆不清的关键也在这里,没有区分语言和立体构造的责任。一个趋势是语言本身总是试图包揽建筑师的责任。把立体构造独立出来,带来的问题是:这个构造本身必须能够证明自己是正确的。1)能产生软件2)构造逻辑上正确,确实能解决应用问题。构造本身有一个属性,它有通用性。根本原理是通用的;总体构造本身具有一般性,也就是抽象性、实际问题无关性;局部构件具有通用性。也就是说,这里存在容器和容量的区别,构造是容器,实际问题是装在容器中的量。一个好的容器要能顶住容量的压力;一个好的建筑架构要能满足负载和抗振性要求。而架构本身的承受能力是客观的,只与架构本身有关。这也就是说,架构本身自我构造的,因此也就是科学。可能软件构造本身是澄清问题的工作,明确“容量”的特点,为软件构造的选择提供准确的依据,杀鸡不要用牛刀。实际问题的“容量”很容易测量,因为它反映为应用的规模,流程的流量。(架构是什么?架构是否存在?如果我们所说非虚,那么如何为架构下一个定义-一定是一个由具体业务流量和模式支撑的架构) 软件(算法)的构造。一个是数据的复杂性(内在互相关系),一个是计算方法(步骤和缓冲)。从宏观角度,数据关系是更根本的东西。目前的高级语言,变量和流程(顺序、分支-步骤;循环-缓冲和迭代)研究的多,而数据复杂性构造不足。 同构现象。CPU指令集合可以说是硬件直接实现的软件软件帝国从这里提取软件精神,并升华它。从硬件的角度,从寄存器和指令执行流程,体现出的是变量和迭代(顺序更迭,循环往复)。(迭代流程)基于固定寻址的变量,经过寻址接口,可以处理任意数据,从而把迭代流程变成了一般流程。CPU的基本过程,产生了指令和数据,指令天生具有子程序的基因(一般流程),数据天生具有数据结构(寻址能力)的基因。高级的构造一般也是这种结构的类似:设计一套类似CPU的机制,支撑程序和数据;独特的“寻址机制”和“CPU处理能力”是实现构造的核心机制;迭代是所有这种机制的动力学和构造方式。而数据化是“寻址机制”的基础。抽象是数据化的工厂,也因此必须研究抽象技术。 抽象技术。所谓抽象,就是具体化,是范围的界定和比对(两种具体化对象之间的比对)。如果范围界定的完整,那么比对建立的联系就是普遍联系,普遍联系也就是所谓抽象原则。 评价标准。软件架构需要评测。这种评测是“在商言商”似的评测。评测的基础是软件架构的具体化。当掌握了架构的构造方法,每种架构本身也就具体化,是一种具体的架构。一种具体化的架构,就可以识别;可以识别则可以客观评测。可以按照立体架构的“压力”、“流量”等概念进行评测。 需求的把握-需求的变化。我们希望永恒不变的需求,核心需求和需求方式(表现和满足步骤);而事实上需求总在演化。软件必须无条件、最大限度地方便需求的表达和需求的满足。软件可能永远只是皮肤,需求源于现实核心深处,软件是一件衣服。这种观点下,软件是没有中心的一种架构。软件架构和需求之间联系的定量评测。 软件和算法的分开 软件的构造作为软件的通用属性 需求的独立 推论:算法是应用的算法。比如数学公式的计算、图形图象的处理、频谱分析、词法和语法分析。因此算法不是通用的软件算法。也因此软件构造是软件规范的一部分,因为它是通用的软件构造技术。 计算技术和应用之间有明显的区别,是两种不同的成分。软件规范是纯粹的,只关心计算技术。而不关心应用建模。计算方法本身早已经被发现了(也就是怎么自动计算,或者说什么是可计算的),剩下的问题只是应用问题。把应用问题的解决纳入软件计算模式。自动计算技术在汇编指令集合那里得到了说明。所谓软件设计是把这种计算方式发扬广大。 所谓算法,就是明确问题,然后发现用自动计算的方式解决问题。从这个意义上说,软件是应用问题导向的。那么,也就是要以问题为中心谈论软件。不同类型的问题需要的解决方式有独特的强调。这也就反映为所谓不同的软件技术。所以,区分软件计算技术和应用问题的成分,是软件规范需要首先识别的东西。 解决问题。本质上是把问题装到变量里面的过程,是放大CPU寄存器的过程。表示层:(把局面、环境;起点和终点需要定义在一个世界里)装进去,组织起来。计算层(展开层):基于表示,定义问题解决步骤(定义运动和过程)。 需求分析。问题描述采用的方法可能应该和软件算法完全分开。否则不能发现问题描述的创造性方法,不能表达问题本质。阐述问题,写文章我们有某篇布局之法;哲学研究我们有严谨的逻辑方法。需求分析,我们一定可以创造自己的方法。这是什么方法?满足使用要求,满足使用流程。离散/隔离各个需求。事实上,面向外部的分析理解和面向内部的分析理解之间有鸿沟。因为这是两个不同的世界。在两个相差悬殊的世界之间,搭建的构造也必然多种多样,以奇为平常。那么,建立联系的媒介少的可怜。可能问题本身也正在于这种联系的分析和设计。 软件的量,是静态的。强调这部分就忽略了活跃的、奇异的、动态的部分。软件的出现不仅仅是被动地适应显示需求,同时也改变了现实需求本身。这种和现实需求融合在一起形成的状态,正是软件活跃的部分。在以前,仅仅以“应用软件”指称是不够的。(操作系统、编译软件、应用软件) 在范畴上,分为三个层次,或说3个范畴域: 1、 活跃的、黏性的动态层次。应用层。和现实之间的界面,是设备逻辑。需求简化、解决方案的奇异性;应用算法的专业性。这是软件形象最活跃的部分。 这里用的是抽象(业务流程)和具体(设备能力)统一的思维方法,构造逻辑的软件过程同时又是可以用具体进行描述的;动态的、物理的分析手段(物理的量)。 业务流程的设计几乎就是艺术设计。 2、 中间层。程序构造层。语言、编译技术、数据结构、设计方法(过程、数据、对象)等可以形式化的计算机科学的任务。对程序能力进行抽象,设计程序自动化生成的一套系统:语言、计算系统、编译系统。这是在静态和活跃部分之间的层次。这里的观念:设计方法、主程序、程序过程(和应用层的过程不是一一对应的)。 3、 静态层。软件的量,度量层。所有程序构造过程的差别消失了。这是软件的静态观点。 每层都有对软件的自己的理念,概念、过程和模型。两个层的对比,则凸显出不可调和的差别。也是所有关于软件的不成熟的印象、抽象产生的地方。 在应用层,抽象的、逻辑过程强一些。想象的部分占据主要的部分。需要对现实的业务,基于设备的具体能力,进行构造。 3个范畴定义了“软件”和“程序”的分别。第1层和第3层论述的是“软件”,而第2层论述的是“程序”。 软件和程序的研究方法不同。程序研究方法是完备的,而软件不完备。 程序开发应当体现软件特性。1)是逻辑的过程,总体的过程和子过程的观察和校验程序。2)软件的量层次上,软件的规模、运行强度和稳定性指标的自测试程序。 第二阶段 一定要有一个标准。软件如衣服,软件的交付文档应当显示出衣服是如何编织起来的。(相对于需求,软件是衣服,非核心;相对于硬件,软件是衣服,包裹) 要有一个理论说明。 架构也是衣服的一个部件,类似衣服的连接方式,模块集合的重心比对。 衣服是一个没有核心的结构。软件也一样要显示出这个特性。 无论如何,我们需要有观察软件的眼光,无论一套软件依据什么样的理论产生。 什么是软件?描述是软件的存在形式(文本格式)。软件一定是可执行的(这是软件的严肃性,精确、定量)。软件是异化的,一般异化为具体、特例(对抽象力最好的归结方式)(没有完美满足需求的软件,相对于需求,软件只能满足固定的需求,而不能满足需求的变化,即一款软件总是具体的;由一般产生出具体的思考方法,也就是构造的方法;或着是磁力打造,一个好的理论一定对现实素材有吸引力,向磁铁一般;这也是在矛盾中建造现实的方法,只要是具体的就肯定是可以分析出潜在矛盾、不完美的,问题不仅仅是分析、认识现实,还要能够构造现实;不存在完美的现实,只存在完美的理论 科学研究的方法是简化。工程的方法是‘相似’,复制发现事物时的状态,那么事物的表现就会复现。 在具体化这里,软件和硬件工作的方法在结果上实现了一致。只是方向不同,软件是从一般进行到具体;硬件一开始就是从具体出发,层层构造,搭建系统。硬件的设计明显具有以工艺、器件为核心的特征。配合器件的特新,进行外围设计。在硬件领域,是‘具体’为上;在软件领域,是‘具体’为下。) 对具体性的解释:组成所有物资的电子、质子、中子是圆的、相同的,但是这些相同的东西组成的原子则有几百种不同。每次量的规模的添加,都导致特殊性的添加。对于软件来说,也是如此。如下的概念是母庸质疑的,软件如同大山,沟壑鲜明。(这种巨大的特殊性,一定是和巨大的需求特殊性相应的)。 “软件以文本形式存在;软件在执行着;软件以个例的形式存在”,归结为在一起就是“软件是具体的”。 低一级别的定义:软件与数据和逻辑相关(数据和逻辑是软件的基本语义)。软件与过程相关(积分(存储,数据的数据化)和步骤(逻辑);过程是步骤的遍历,是数据的消长变化)。 执行的异化。区分独立执行和整体执行的概念。独立执行的代码称为模块,否则只是‘片段’。独立性和数据完整性相关,数据越庞大那么不独立的代码片段越多,模块就越大。模块独立性具有比和整体执行所要求的更大的自由度,也就是说整体只是使用了模块一部分的执行能力。模块独立执行获得的自由度是应该能够度量;模块的执行设计应该为了获取更大的自由度;自由度是模块可执行性质量的评定指标。对于整体执行的设计来说,自由度设计可能是设计过程的主导方法,它和全面、完整的需求理解相关,也和需求变化相关;因此自由度设计也是需求定位的设计。 软件的量,也就是软件的能力。这是理解软件解决问题的方式的基础。比如逻辑能力、计算能力、存储能力、图象能力等。 软件是运行的,软件是自我构造的,软件的全体的各个环节都有自己的量。编译、操作系统、文件管理等各环节都是不同分工的软件实现的。 需要构造在功能层次上的互相配合,解释这种完整性。显然每个部分都具有独立的完整性;完整性和完整性的配合构成一个总体的系统。因此未必要求系统的完整性、长期性、稳定性。反过来,系统满足需求的快速性、快速变化适应性、和现实一起变化、消长的特性、瞬态响应特性可能更接近系统的本质。 这好比太极拳,要在一个完满的氛围里运动。 软件能力是比代码高一个级别的抽象。又是构成软件内涵的基础语义。 ‘设备能力’的概念更基础,可以统一所有其它能力;又可以作为以硬件为中心的观念的基础。 能力的获得在于‘二分’。在于互相支撑的界面,支撑在一起的双方互为能力。 1.所谓需求分析,我们总是在创造一套新的方法和语言。而最有效的需求分析是自然语言分析。借助人们心目中的全部理解所用到的描述形式。也就是进入到实际存在的需求中去理解需求,分析需求。 因为领域、术语、行业表述习惯的原因,这个阶段千差万别。 2.其次是电脑的使用方式-电脑技术(外设、通信和电脑本身的硬件形态),尝试去设计合适的使用方式和硬件解决方案。 这里有使用环境、专业技术、成本、时间,以及个人习惯等原因,同样是一个精彩的过程。对领域工作方式的熟悉、外设相关的专业技术背景、折中技术决定了这是一个经验至上的活动。这就是电脑使用方式的确定。 3.进一步,确定使用者角色。使用者和使用地点关联。使用地点也就是前面电脑使用方式的一部分。 这是一个沟通过程,也是对有了电脑辅助参与,相关领域习惯改革的问题。 4.然后,进入二元分析阶段:使用者管角度、客观功能角度,分析功能,并完成二者之间的映射。 这个阶段,功能被量化。职能量化。职能和功能之间会有模糊,有授权的转移。这个阶段就是充分考虑这些问题。 5.然后,进入传统的需求分析阶段。 计算架构和功能描述的规格分析。使用者界面规划(详细、规格级别)。 界面规划、功能、架构三者之间组成互动的具体化过程。 最后会产生系统级别的文档。运行实体、接口;系统运行态、实体接口的输入输出规格。 6.然后,实体级别的程序构造阶段。 算法构造和程序构造。主要是从资源占用的角度确定宏观的算法。在这个阶段,是程序文档化阶段。文档这个属于是这个阶段的工具。 最后会产生严格的程序模块的文档。所有这些文档组合起来,可以构成运行流程。这些文档化的程序就是逻辑化的程序本身。 7.最后,编码阶段 用一种具体的语言,按照模块文档的接口、资源、算法要求,编制代码。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值