《程序员修炼之道》——第二章 注重实效的途径(三)
九、 可撤销性
有许多人会设法保持代码的灵活性,而你还需要考虑维持架构、部署及供应商集成等领域的灵活性。
通常你可以把第三方产品隐藏在定义良好的抽象接口后面。事实上,在我们做过的任何项目中,我们都总能够这么做。但假定你无法那么彻底地隔离它,如果你必须大量地把某些语句分散在整个代码中,该怎么办?把需求放入元数据,并且使用某种自动机制——比如Aspect或Perl——把必要的语句插入代码自身中。无论你使用的是何种机制,让它可撤销。如果某样东西是自动添加的,他就可以被自动去掉。
十、曳光弹
在黑暗中用机枪射击可以用曳光弹的方式。曳光弹与常规弹药交错着装在弹药带上。发射时,曳光弹中的磷点燃,在枪与它们击中的地方之间留下一条烟火般的踪迹。如果曳光弹击中目标,那么常规子弹也会击中目标。
这个类比也许有些暴力,蛋它适用于新的项目,特别是你构建从未构建过的东西时。与枪手一样,你也设法在黑暗中击中目标。因为你的用户从未见过这样的系统,他们的需求可能会含糊不清。因为你在使用不熟悉的算法、技术、语言和库,你面对着大量未知的事物。同时,因为完成项目需要时间,在很大程度上你能够通知,你的工作环境将在你完成之前发生变化。
在黑暗中发光的代码
曳光弹行之有效,是因为它们与真正的子弹在相同的环境、相同的约束下工作。它们快速飞向目标,所以抢手可以得到即时的反馈。同时,从实践的角度看,这样的解决方案也更便宜。
为了在代码中获得同样的效果,我们要找到某种东西,让我们能快速、直观和可重复地从需求出发,满足最终系统的某个方面要求。
有一次,我们接受了一个复杂的客户-服务器数据库营销项目。其部分需求是要能够指定并执行临时查询。服务器是一系列专用的关系数据库。用Object Pascal编写的客户GUI使用一组C库提供给服务器的接口。在转换为优化的SQL之前,用户的查询以类似Lisp的表示方式存储在服务器上;转换直到执行前才进行。有许多未知因素和许多不同的环境,没有人清楚地直到GUI应该怎样工作。
这是使用曳光代码的好机会。我们开发了前端框架、用于查询的库以及用于把所存储的查询转换为具体数据库的查询的结构。随后我们把它们几种在一起,并检查它们是否能工作。使用最初构建的系统,我们所能做的只是提交一个查询,列出某个列表中的所有行,但它证明了UI能够与库交谈,库能够对查询进行序列化和解序列化,而服务器能够根据结果生成SQL。在接下啦的几个月里,我们逐渐充实这个基本结构,通过并行地扩大曳光代码的各个组件增加新的功能。当UI增加了新的查询类型时,库随之成长,而我们也使SQL生成变得更为成熟。
曳光代码并非用过就扔的代码:你编写它,是为了保留它。它含有任何一段代码产品都拥有的完整的错误检查、结构、文档、以及自查。它只不过功能不全而已。但是,一旦你在系统的各组件间实现了端到端的连接,你就可以检查你离目标还有多远,并在必要的情况下进行调整。一旦你完全瞄准,增加功能将是一件容易的事。
曳光开发与项目永不会结束的理念是一致的:总有改动需要完成,总有功能需要增加。这是一个渐进的过程。
另一种传统方法是一种繁重的工程方法:把代码划分为模块,在真空中对模块进行编码,把模块组合成子配件,再对自配件进行组合,直到有一天你拥有完整的应用为止。直到那时,才能把应用作为一个整体呈现给用户,并进行测试。
曳光代码方法有许多优点:
- 用户能够及早看到能工作的东西。如果你成功地就你在做的事情与用户进行了交流,用户就会知道他们看到的是还未完成的东西。他们不会因为缺少功能而失望;他们将因为看到了系统的某种可见的进展而欣喜陶醉。他们还会随着项目的进展做出贡献,增加他们的“买入”。同样是这些用户,他们很可能也会告诉你,每一轮“射击“距离目标有多接近。
- 开发者构建了一个他们能在其中工作的结构。最令人畏缩的纸是什么也没写的白纸。如果你已经找出应用的所有端到端的交互,并把它们体现在代码里,你的团队就无须再无中生有。这让每个人都变得更有生产力,同时又促进了一致性。
- 你有了一个集成平台。随着系统端到端地连接起来,你拥有了一个环境,一旦新的代码段通过了单元测试,你就可以将其加入该环境中。你将每天进行集成(常常是一天进行多次),而不是尝试进行大爆炸式的集成。每个新改动的影响都更为显而易见,而交互也更为有限,于是调试和测试将变得更快、更准确。
- 你有了可用于演示的东西。项目出资人与高级官员往往会在最不方便的时候来看演示。有了曳光代码,你总有东西可以拿给他们看。
- 你将能够感受到工作进展。在曳光代码开发中,开发者一个一个地处理用例。做完一个,再做下一个。评测性能、并向用户演示你的进展,变得容易了许多。因为每一项个别的开发都更小,你也避免了创建这样的整体式代码块:一周又一周,其完成度一直是95%。
曳光弹并非总能命中目标
曳光弹告诉你击中的是什么。那不一定总是目标。于是你调整准星,直到完全击中目标为止。这正是要点所在。
曳光代码也是如此。你在不能100%确定该去往何处的情形下使用这项技术。如果最初的几次尝试错过了目标——用户说:“那不是我的意思”,你需要的数据在你需要它时不可用,或是性能好像有问题——你不应感到惊奇。找出怎样改变已有的东西、让其更接近于目标的办法,并且为你使用了一种简约的开发方法而感到高兴。小段代码的惯性也小——要改变它更容易、更迅速。你能够搜集关于你应用的反馈,而且与其他任何方法相比,你能够话费较小的代价、更为迅速地生成新的、更为准确的版本。同时,因为每个主要的应用组件都已表现在你的曳光代码中,用户可以确信,他们所看到的东西具有现实基础,不仅仅是纸上的规范。
曳光代码 vs 原型制作
你也许会想,这种曳光代码的概念就是原型制作,只不过有一个更富“进攻性”的名字。他们有所区别。使用原型,你是要探究最终系统的某些的方面。使用真正的原型,在对概念进行了实验之后,你会把你捆扎在一起的无论什么东西扔掉,并根据你学到的经验教训重新适当地进行编码。
例如,假定你在制作一个应用,其用途是帮助运货人确定怎样把不规则的箱子装入集装箱。
除了考虑其他一些问题,你还需要设计直观的用户界面,而你用于确定最优集装箱方式的算法非常复杂。
你可以在GUI工具中为最终用户制作一个用户界面原型。你的代码只能让界面响应用户操作。一旦用户对界面布局表示同意,你可以把它扔掉,用目标语言重新对其进行编码,并在其后加上商业逻辑。与此类似,你可以为实际进行装箱的算法制作原型。你可以用像Perl这样的宽松的高级语言编写功能测试,并用更接近机器的某种语言编写低级的性能测试。无论如何,一旦你做出决策,你都会重新开始在其最终环境中为算法编写代码,与现实世界接合。这就是原型制作,它非常有用。
曳光代码方法处理的是不同的问题。你需要知道应用怎样结合成一个整体。你想要向用户演示,实际的交互式怎样工作的,同时你还想要给出一个结构骨架,开发者可以在其上增加代码。在这样的情况下,你可以构造一段曳光代码,其中含有一个极其简单的集装箱装箱算法实现和一个简单算法、但却能工作的用户界面。一旦你把应用中的所有组件都组合在一起,你就拥有了一个可以向你的用户和开发者演示的框架。接下来的时间里,你给这个框架增加新功能,完成预留了接口的例程。但框架仍保持完整,而你也知道,系统将会继续按照你第一次的曳光代码完成时的方式工作。
其间的区别非常重要,足以让我们再重复一次。原型制作生成用过就扔的代码。曳光代码虽然简约,但却是完整的,并且构成了最终系统的骨架的一部分。你可以把原型制作视为在第一次曳光弹发射之前进行的侦察和情报搜集的工作。