必读网 - 人生必读的书

TXT下载此书 | 书籍信息


(双击鼠标开启屏幕滚动,鼠标上下控制速度) 返回首页
选择背景色:
浏览字体:[ ]  
字体颜色: 双击鼠标滚屏: (1最慢,10最快)

程序员修炼之道

_4 Andrew(美)
  并不让人惊奇的是,曳光弹比费力计算更可取。反馈是即时的,而且因为它们工作在与真正的弹药相同的环境中,外部影响得以降至最低。
  这个类比也许有点暴力,但它适用于新的项目,特别是当你构建从未构建过的东西时。与枪手一样,你也设法在黑暗中击中目标。因为你的用户从未见过这样的系统,他们的需求可能会含糊不清。因为你在使用不熟悉的算法、技术、语言或库,你面对着大量未知的事物。同时,因为完成项目需要时间,在很大程度上你能够确知,你的工作环境将在你完成之前发生变化。
  经典的做法是把系统定死。制作大量文档,逐一列出每项需求、确定所有未知因素、并限定环境。根据死的计算射击。预先进行一次大量计算,然后射击并企望击中目标。
  然而,注重实效的程序员往往更喜欢使用曳光弹。
在黑暗中发光的代码
  曳光弹行之有效,是因为它们与真正的子弹在相同的环境、相同的约束下工作。它们快速飞向目标,所以枪手可以得到即时的反馈。同时,从实践的角度看,这样的解决方案也更便宜。
  为了在代码中获得同样的效果,我们要找到某种东西,让我们能快速、直观和可重复地从需求出发,满足最终系统的某个方面要求。
提示15
Use Tracer Bullets to Find the Target
用曳光弹找到目标
  有一次,我们接受了一个复杂的客户-服务器数据库营销项目。其部分需求是要能够指定并执行临时查询。服务器是一系列专用的关系数据库。用Object Pascal编写的客户GUI使用一组C库提供给服务器的接口。在转换为优化的SQL之前,用户的查询以类似Lisp的表示方式存储在服务器上;转换直到执行前才进行。有许多未知因素和许多不同的环境,没有人清楚地知道GUI应该怎样工作。
  这是使用曳光代码的好机会。我们开发了前端框架、用于表示查询的库以及用于把所存储的查询转换为具体数据库的查询的结构。随后我们把它们集中在一起,并检查它们是否能工作。使用最初构建的系统,我们所能做的只是提交一个查询,列出某个表中的所有行,但它证明了UI能够与库交谈,库能够对查询进行序列化和解序列化,而服务器能够根据结果生成SQL。在接下来的几个月里,我们逐渐充实这个基本结构,通过并行地扩大曳光代码的各个组件增加新的功能。当UI增加了新的查询类型时,库随之成长,而我们也使SQL生成变得更为成熟。
  曳光代码并非用过就扔的代码:你编写它,是为了保留它。它含有任何一段产品代码都拥有的完整的错误检查、结构、文档、以及自查。它只不过功能不全而已。但是,一旦你在系统的各组件间实现了端到端(end-to-end)的连接,你就可以检查你离目标还有多远,并在必要的情况下进行调整。一旦你完全瞄准,增加功能将是一件容易的事情。
  曳光开发与项目永不会结束的理念是一致的:总有改动需要完成,总有功能需要增加。这是一个渐进的过程。
  另一种传统做法是一种繁重的工程方法:把代码划分为模块,在真空中对模块进行编码。把模块组合成子配件(subassembly),再对子配件进行组合,直到有一天你拥有完整的应用为止。直到那时,才能把应用作为一个整体呈现给用户,并进行测试。
  曳光代码方法有许多优点:
l 用户能够及早看到能工作的东西。如果你成功地就你在做的事情与用户进行了交流(参见“极大的期望”,255页),用户就会知道他们看到的是还未完成的东西。他们不会因为缺少功能而失望;他们将因为看到了系统的某种可见的进展而欣喜陶醉。他们还会随着项目的进展做出贡献,增加他们的“买入”。同样是这些用户,他们很可能也会告诉你,每一轮“射击”距离目标有多接近。
l 开发者构建了一个他们能在其中工作的结构。最令人畏缩的纸是什么也没有写的白纸。如果你已经找出应用的所有端到端的交互,并把它们体现在代码里,你的团队就无须再无中生有。这让每个人都变得更有生产力,同时又促进了一致性。
l 你有了一个集成平台。随着系统端到端地连接起来,你拥有了一个环境,一旦新的代码段通过了单元测试,你就可以将其加入该环境中。你将每天进行集成(常常是一天进行多次),而不是尝试进行大爆炸式的集成。每一个新改动的影响都更为显而易见,而交互也更为有限,于是调试和测试将变得更快、更准确。
l 你有了可用于演示的东西。项目出资人与高级官员往往会在最不方便的时候来看演示。有了曳光代码,你总有东西可以拿给他们看。
l 你将更能够感觉到工作进展。在曳光代码开发中,开发者一个一个地处理用例(use case)。做完一个,再做下一个。评测性能、并向用户演示你的进展,变得容易了许多。因为每一项个别的开发都更小,你也避免了创建这样的整体式代码块:一周又一周,其完成度一直是95%。
曳光弹并非总能击中目标
  曳光弹告诉你击中的是什么。那不一定总是目标。于是你调整准星,直到完全击中目标为止。这正是要点所在。
  曳光代码也是如此。你在不能100%确定该去往何处的情形下使用这项技术。如果最初的几次尝试错过了目标——用户说:“那不是我的意思”,你需要的数据在你需要它时不可用,或是性能好像有问题——你不应感到惊奇。找出怎样改变已有的东西、让其更接近目标的办法,并且为你使用了一种简约的开发方法而感到高兴。小段代码的惯性也小——要改变它更容易、更迅速。你能够搜集关于你的应用的反馈,而且与其他任何方法相比,你能够花费较少代价、更为迅速地生成新的、更为准确的版本。同时,因为每个主要的应用组件都已表现在你的曳光代码中,用户可以确信,他们所看到的东西具有现实基础,不仅仅是纸上的规范。
曳光代码 vs. 原型制作
  你也许会想,这种曳光代码的概念就是原型制作,只不过有一个更富“进攻性”的名字。它们有区别。使用原型,你是要探究最终系统的某些具体的方面。使用真正的原型,在对概念进行了试验之后,你会把你捆扎在一起的无论什么东西扔掉,并根据你学到的经验教训重新适当地进行编码。
  例如,假定你在制作一个应用,其用途是帮助运货人确定怎样把不规则的箱子装入集装箱。
除了考虑其他一些问题,你还需要设计直观的用户界面,而你用于确定最优装箱方式的算法非常复杂。
  你可以在GUI工具中为最终用户制作一个用户界面原型。你的代码只能让界面响应用户操作。一旦用户对界面布局表示同意,你可以把它扔掉,用目标语言重新对其进行编码,并在其后加上商业逻辑。与此类似,你可以为实际进行装箱的算法制作原型。你可以用像Perl这样的宽松的高级语言编写功能测试,并用更接近机器的某种语言编写低级的性能测试。无论如何,一旦你做出决策,你都会重新开始在其最终环境中为算法编写代码,与现实世界接合。这就是原型制作,它非常有用。
  曳光代码方法处理的是不同的问题。你需要知道应用怎样结合成一个整体。你想要向用户演示,实际的交互是怎样工作的,同时你还想要给出一个架构骨架,开发者可以在其上增加代码。在这样的情况下,你可以构造一段曳光代码,其中含有一个极其简单的集装箱装箱算法实现(也许是像“先来先服务”这样的算法)和一个简单、但却能工作的用户界面。一旦你把应用中的所有组件都组合在一起,你就拥有了一个可以向你的用户和开发者演示的框架。接下来的时间里,你给这个框架增加新功能,完成预留了接口的例程。但框架仍保持完整,而你也知道,系统将会继续按照你第一次的曳光代码完成时的方式工作。
  其间的区别很重要,足以让我们再重复一次。原型制作生成用过就扔的代码。曳光代码虽然简约,但却是完整的,并且构成了最终系统的骨架的一部分。你可以把原型制作视为在第一发曳光弹发射之前进行的侦察和情报搜集工作。
相关内容:
l 足够好的软件,9页
l 原型与便笺,53页
l 规范陷阱,217页
l 极大的期望,255页
11 原型与便笺
  许多不同的行业都使用原型试验具体的想法:与完全的制作相比,制作原型要便宜得多。例如,轿车制造商可以制造某种新车设计的许多不同的原型,每一种的设计目的都是要测试轿车的某个具体的方面——空气动力学、样式、结构特征,等等。也许会制造一个粘土模型,用于风洞测试,也许会为工艺部门制造一个轻木和胶带模型,等等。有些轿车公司更进一步,在计算机上进行大量的建模工作,从而进一步降低了开销。以这样的方式,可以试验危险或不确定的元件,而不用实际进行真实的制造。
  我们以同样的方式构建软件原型,并且原因也一样——为了分析和揭示风险,并以大大降低的代价、为修正提供机会。与轿车制造商一样,我们可以把原型用于测试项目的一个或多个具体方面。
  我们往往以为原型要以代码为基础,但它们并不总是非如此不可。与轿车制造商一样,我们可以用不同的材料构建原型。要为像工作流和应用逻辑这样的动态事物制作原型,便笺(post-it note)就非常好。用户界面的原型则可以是白板上的图形、或是用绘图程序或界面构建器绘制的无功能的模型。
  原型的设计目的就是回答一些问题,所以与投入使用的产品应用相比,它们的开发要便宜得多、快捷得多。其代码可以忽略不重要的细节——在此刻对你不重要,但对后来的用户可能非常重要。例如,如果你在制作GUI原型,你不会因不正确的结果或数据而遭到指责。而另一方面,如果你只是在研究计算或性能方面的问题,你也不会因为相当糟糕的GUI而遭到指责;甚至也可以完全不要GUI。
  但如果你发现自己处在不能放弃细节的环境中,就需要问自己,是否真的在构建原型。或许曳光弹开发方式更适合这种情况(参见“曳光弹”,48页)。
应制作原型的事物
  你可以选择通过原型来研究什么样的事物呢?任何带有风险的事物。以前没有试过的事物,或是对于最终系统极端关键的事物。任何未被证明的、实验性的、或有疑问的事物。任何让你觉得不舒服的事物。你可以为下列事物制作原型:
l 架构
l 已有系统中的新功能
l 外部数据的结构或内容
l 第三方工具或组件
l 性能问题
l 用户界面设计
  原型制作是一种学习经验。其价值并不在于所产生的代码,而在于所学到的经验教训。那才是原型制作的要点所在。
提示16
Prototype to Learn
为了学习而制作原型
怎样使用原型
  在构建原型时,你可以忽略哪些细节?
l 正确性。你也许可以在适当的地方使用虚设的数据。
l 完整性。原型也许只能在非常有限的意义上工作,也许只有一项预先选择的输入数据和一个菜单项。
l 健壮性。错误检查很可能不完整,或是完全没有。如果你偏离预定路径,原型就可能崩溃,并在“烟火般的灿烂显示中焚毁”。这没有关系。
l 风格。在纸上承认这一点让人痛苦,但原型代码可能没有多少注释或文档。根据使用原型的经验,你也许会撰写出大量文档,但关于原型系统自身的内容相对而言却非常少。
  因为原型应该遮盖细节,并聚焦于所考虑系统的某些具体方面,你可以用非常高级的语言实现原型——比项目的其余部分更高级(也许是像Perl、Python或Tcl这样的语言)。高级的脚本语言能让你推迟考虑许多细节(包括指定数据类型),并且仍然能制作出能工作的(即使不完整或速度慢)代码。如果你需要制作用户界面的原型,可研究像Tcl/Tk、Visual Basic、Powerbuilder或Delphi这样的工具。
  作为能把低级的部分组合在一起的“胶合剂”,脚本语言工作良好。在Windows下,Visual Basic可以把COM控件胶合在一起。更一般地说,你可以使用像Perl和Python这样的语言,把低级的C库绑在一起——无论是手工进行,还是通过工具自动进行,比如可以自由获取的SWIG[URL 28]。采用这种方法,你可以快速地把现有组件装配进新的配置,从而了解它们的工作情况。
制作架构原型
  许多原型被构造出来,是要为在考虑之下的整个系统建模。与曳光弹不同,在原型系统中,单个模块不需要能行使特定的功能。事实上,要制作架构原型,你甚至不一定需要进行编码——你可以用便笺或索引卡片、在白板上制作原型。你寻求的是了解系统怎样结合成为一个整体,并推迟考虑细节。下面是一些你可以在架构原型中寻求解答的具体问题:
l 主要组件的责任是否得到了良好定义?是否适当?
l 主要组件间的协作是否得到了良好定义?
l 耦合是否得以最小化?
l 你能否确定重复的潜在来源?
l 接口定义和各项约束是否可接受?
l 每个模块在执行过程中是否能访问到其所需的数据?是否能在需要时进行访问?
根据我们制作原型的经验,最后一项往往会产生最让人惊讶和最有价值的结果。
怎样“不”使用原型
  在你着手制作任何基于代码的原型之前,先确定每个人都理解你正在编写用过就扔的代码。对于不知道那只是原型的人,原型可能会具有欺骗性的吸引力。你必须非常清楚地说明,这些代码是用过就扔的,它们不完整,也不可能完整。
  别人很容易被演示原型外表的完整性误导,而如果你没有设定正确的期望值,项目出资人或管理部门可能会坚持要部署原型(或其后裔)。提醒他们,你可以用轻木和胶带制造一辆了不起的新车原型,但你却不会在高峰时间的车流中驾驶它。
  如果你觉得在你所在的环境或文化中,原型代码的目的很有可能被误解,你也许最好还是采用曳光弹方法。你最后将得到一个坚实的框架,为将来的开发奠定基础。
  适当地使用原型,可以帮助你在开发周期的早期确定和改正潜在的问题点——在此时改正错误既便宜、又容易——从而为你节省大量时间、金钱,并大大减轻你遭受的痛苦和折磨。
相关内容:
l 我的源码让猫给吃了,2页
l 交流!,18页
l 曳光弹,48页
l 极大的期望,255页
练习
4. 市场部门想要坐下来和你一起讨论一些网页的设计问题。他们想用可点击的图像进行页面导航,但却不能确定该用什么图像模型——也许是轿车、电话或是房子。你有一些目标网页和内容;他们想要看到一些原型。哦,随便说一下,你只有15分钟。你可以使用什么样的工具?  (解答在280页)
12 领域语言
语言的界限就是一个人的世界的界限。
  ——维特根斯坦
  计算机语言会影响你思考问题的方式,以及你看待交流的方式。每种语言都含有一系列特性——比如静态类型与动态类型、早期绑定与迟后绑定、继承模型(单、多或无)这样的时髦话语——所有这些特性都在提示或遮蔽特定的解决方案。头脑里想着Lisp设计的解决方案将会产生与基于C风格的思考方式而设计的解决方案不同的结果,反之亦然。与此相反——我们认为这更重要——问题领域的语言也可能会提示出编程方案。
  我们总是设法使用应用领域的语汇来编写代码(参见210页的需求之坑,我们在那里提出要使用项目词汇表)。在某些情况下,我们可以更进一层,采用领域的语汇、语法、语义——语言——实际进行编程。
  当你听取某个提议中的系统的用户说明情况时,他们也许能确切地告诉你,系统应怎样工作:
在一组X.25线路上侦听由ABC规程12.3定义的交易,把它们转译成XYZ公司的43B格式,在卫星上行链路上重新传输,并存储起来,供将来分析使用。
  如果用户有一些这样的做了良好限定的陈述,你可以发明一种为应用领域进行了适当剪裁的小型语言,确切地表达他们的需要:
From X25LINE1 (Format=ABC123) {
Put TELSTAR1 (Format=XYZ43B);
Store DB;
}
  该语言无须是可执行的。一开始,它可以只是用于捕捉用户需求的一种方式——一种规范。但是,你可能想要更进一步,实际实现该语言。你的规范变成了可执行代码。
  在你编写完应用之后,用户给了你一项新需求:不应存储余额为负的交易,而应以原来的格式在X.25线路上发送回去:
From X25LINE1 (Format=ABC123) {
if (ABC123.balance < 0) {
Put X25LINE1 (Format=ABC123);
}
else {
Put TELSTAR1 (Format=XYZ43B);
Store DB;
}
}
  很容易,不是吗?有了适当的支持,你可以用大大接近应用领域的方式进行编程。我们并不是在建议让你的最终用户用这些语言实际编程。相反,你给了自己一个工具,能够让你更靠近他们的领域工作。
提示17
Program Close to the Problem domain
靠近问题领域编程
  无论是用于配置和控制应用程序的简单语言,还是用于指定规则或过程的更为复杂的语言,我们认为,你都应该考虑让你的项目更靠近问题领域。通过在更高的抽象层面上编码,你获得了专心解决领域问题的自由,并且可以忽略琐碎的实现细节。
  记住,应用有许多用户。有最终用户,他们了解商业规则和所需输出;也有次级用户:操作人员、配置与测试管理人员、支持与维护程序员,还有将来的开发者。他们都有各自的问题领域,而你可以为他们所有人生成小型环境和语言。
具体领域的错误
  如果你是在问题领域中编写程序,你也可以通过用户可以理解的术语进行具体领域的验证,或是报告问题。以上一页我们的交换应用为例,假定用户拼错了格式名:
From X25LINE1 (Format=AB123)
  如果这发生在某种标准的、通用的编程语言中,你可能会收到一条标准的、通用的错误消息:
Syntax error: undeclared identifier
  但使用小型语言,你却能够使用该领域的语汇发出错误消息:
"AB123" is not a format. known formats are ABC123,
XYZ43B, PDQB, and 42.
实现小型语言
  在最简单的情况下,小型语言可以采用面向行的、易于解析的格式。在实践中,与其他任何格式相比,我们很可能会更多地使用这样的格式。只要使用switch语句、或是使用像Perl这样的脚本语言中的正则表达式,就能够对其进行解析。281页上练习5的解答给出了一种用C编写的简单实现。
  你还可以用更为正式的语法,实现更为复杂的语言。这里的诀窍是首先使用像BNF这样的表示法定义语法。一旦规定了文法,要将其转换为解析器生成器(parser generator)的输入语法通常就非常简单了。C和C++程序员多年来一直在使用yacc(或其可自由获取的实现,bison[URL 27])。在Lex and Yacc[LMB92]一书中详细地讲述了这些程序。Java程序员可以选用javaCC,可在[URL 26]处获取该程序。282页上练习7的解答给出了一个用bison编写的解析器。如其所示,一旦你了解了语法,编写简单的小型语言实在没有多少工作要做。
  要实现小型语言还有另一种途径:扩展已有的语言。例如,你可以把应用级功能与Python[URL 9]集成在一起,编写像这样的代码:
record = X25LINE1.get(format=ABC123)
if (lance < 0):
X25LINE1.put(record, format=ABC123)
else:
TELSTAR1.put(record, format=XYZ43B)
DB.store(record)
数据语言与命令语言
  可以通过两种不同的方式使用你实现的语言。
  数据语言产生某种形式的数据结构给应用使用。这些语言常用于表示配置信息。
  例如,sendmail程序在世界各地被用于在Internet上转发电子邮件。它具有许多杰出的特性和优点,由一个上千行的配置文件控制,用sendmail自己的配置语言编写:
Mlocal, P=/usr/bin/procmail,
F=lsDFMAw5 :/|@qSPfhn9,
S=10/30, R=20/40,
T=DNS/RFC822/X-Unix,
A=procmail -Y -a $h -d $u
  显然,可读性不是sendmail的强项。
  多年以来,Microsoft一直在使用一种可以描述菜单、widget(窗口小部件)、对话框及其他Windows资源的数据语言。下一页上的图2.2摘录了一段典型的资源文件。这比sendmail的配置文件要易读得多,但其使用方式却完全一样——我们编译它,以生成数据结构。
  命令语言更进了一步。在这种情况下,语言被实际执行,所以可以包含语句、控制结构、以及类似的东西(比如58页上的脚本)。
图2.2 Windows .rc文件
  你也可以使用自己的命令语言来使程序易于维护。例如,也许用户要求你把来自某个遗留应用的信息集成进你的新GUI开发中。要完成这一任务,常用的方法是“刮屏”(screen scraping):你的应用连接到主机应用,就好像它是正常的使用人员;发出键击,并“阅读”取回的响应。你可以使用一种小型语言来把这样的交互编写成脚本:
locate prompt "SSN:"
type "%s" social_security_number
type enter
waitfor keyboardunlock
if text_at(10,14) is "INVALID SSN" return bad_ssn
if text_at(10,14) is "DUPLICATE SSN" return dup_ssn
# etc...
当应用确定是时候输入社会保障号时,它调用解释器执行这个脚本,后者随即对事务进行控制。如果解释器是嵌入在应用中的,两者甚至可以直接共享数据(例如,通过回调机制)。
  这里你是在维护程序员(maintenace programmer)的领域中编程。当主机应用发生变化、字段移往别处时,程序员只需更新你的高级描述,而不用钻入C代码的各种细节中。
独立语言与嵌入式语言
  要发挥作用,小型语言无须由应用直接使用。许多时候,我们可以使用规范语言创建各种由程序自身编译、读入或用于其他用途的制品(包括元数据。参见元程序设计,144页)。
  例如,在100页我们将描述一个系统,在其中我们使用Perl、根据原始的schema规范生成大量衍生物。我们发明了一种用于表示数据库schema的通用语言,然后生成我们所需的所有形式——SQL、C、网页、XML,等等。应用不直接使用规范,但它依赖于根据规范产生的输出。
  把高级命令语言直接嵌入你的应用是一种常见做法,这样,它们就会在你的代码运行时执行。这显然是一种强大的能力;通过改变应用读取的脚本,你可以改变应用的行为,却完全不用编译。这可以显著地简化动态的应用领域中的维护工作。
易于开发还是易于维护
  我们已经看到若干不同的文法,范围从简单的面向行的格式到更为复杂的、看起来像真正的语言的文法。既然实现更为复杂的文法需要额外的努力,你又为何要这样做呢?
  权衡要素是可扩展性与维护。尽管解析“真正的”语言所需的代码可能更难编写,但它却容易被人理解得多,并且将来用新特性和新功能进行扩展也要容易得多。太简单的语言也许容易解析,但却可能晦涩难懂——很像是60页上的sendmail例子。
  考虑到大多数应用都会超过预期的使用期限,你可能最好咬紧牙关,先就采用更复杂、可读性更好的语言。最初的努力将在降低支持与维护费用方面得到许多倍的回报。
相关内容:
l 元程序设计,144页
挑战
l 你目前的项目的某些需求是否能以具体领域的语言表示?是否有可能编写编译器或转译器,生成大多数所需代码?
l 如果你决定采用小型语言作为更接近问题领域的编程方式,你就是接受了,实现它们需要一些努力。你能否找到一些途径,通过它们把你为某个项目开发的框架复用于其他项目?
练习
5. 我们想实现一种小型语言,用于控制一种简单的绘图包(或许是一种“海龟图形”(turtle-graphics)系统)。这种语言由单字母命令组成。有些命令后跟单个数字。例如,下面的输入将会绘制出一个矩形:
P 2 # select pen 2
D # pen down
W 2 # draw west 2cm
返回书籍页