第六章 敏捷编码

任何一个笨蛋都能够让事情变得越来越笨重,越来越极端.需要天才的指点以及许多的勇气,才能让事情向相反的方向发展.

-John Dryden, 书信集10:

广告:个人专属 VPN,独立 IP,无限流量,多机房切换,还可以屏蔽广告和恶意软件,每月最低仅 5 美元

至Congreve新项目刚开始着手开发时,它的代码很容易理解和上手.然而,随着开发过程的推进,项目不知不觉中演变为一个庞然怪物.发展到最后,往往需要投入更多的精力,人力和物力来让它继续下去.

开始看起来非常正常的项目,是什么让它最终变得难以掌控?开发人员在完成任务时,可能会难以底挡诱惑为节省时间而走”捷径”.然而,这些”捷径”往往只会推迟问题的爆发时间,而不是把它彻底解决掉(如同第15页习惯2中的情况一样).当项目时间上的压力增加时,问题最终还是会在项目团队面前出现,让大家心烦意乱.

如何保证项目开发过程中压力正常,而不是在后期面对过多的压力,以致噩梦连连呢?最简单的方式,就是在开发过程中便细心”照看”代码.在编写代码时,每天付出一点小的努力,可以避免代码”腐烂”,并且保证应用程序不至变得难以理解和维护.

开发人员使用本章的实践习惯,可以保证开发出的代码无论是在项目进行中还是在项目完成后,都易于理解,扩展和维护.这些习惯会帮助你对代码进行”健康检查”,以防止它们变成庞然怪物.

首先,第100页中的习惯是:代码要清晰地表达意图.这样的代码清晰易懂,仅凭小聪明写出的程序很难维护.注释可以帮助理解,也可能导致不好的干扰,应该总是用代码沟通(见105页).在工程项目中没有免费的午餐,开发人员必须判断哪些东西更加重要,每个决策会造成的后果,也就是说要动态评估取舍(见第110页)以得到最佳的决策.

项目是以增量方式进行开发的,写程序时也应该进行增量式编程(见第113页).在编写代码的时候,要想保持简单很难做到---实际上,想写出简单的代码要远比写出令人厌恶的,过分复杂的代码难得多,不过这样做绝对值得,见第115页.

我们将在第117页谈到,良好的面向对象设计原则建议:应该编写内聚的代码.要保持代码条理清晰,应该遵循如第121页上所述的习惯:告知,不要询问.最后,通过设计能够根据契约进行替换的系统(见124页),可以在不确定的未来中保持代码的灵活性.

25 代码要清晰地表达意图

“可以工作而且易于理解的代码当然好,但是让人觉得聪明更加重要.别人给你钱是因为你脑子好使,让我们看看你到底有多聪明.”

我们大概都见过不少难以理解和维护的代码.而且(最坏的是)还有错误.当开发人员们像一群旁观见到UFO一样围在代码四周,同样也感到恐惧,困惑与无助时,这个代码的质量就可想而知了.如果没有人理解一段代码的工作方式,那这段代码还有什么用呢?

开发代码时,应该更注重可读性,而不是只图自己方便.代码阅读的次数要远远超过编写的次数和,所以在编写的时候值得花点功夫让它读起业更加简单.实际上,从衡量标准上来看,代码清晰程度的优先级应该排在执行效率之前.

例如,如果默认参数或可选参九会影响代码可读性,使其更难以理解和调试,那最好明确地指明参数,而不是在以后让人觉得迷惑。

在改动代码以修复bug或者添加新功能时,应该有条不紊地进行。首先,应该理解代码做什么,它是如何做的。接下来,搞清楚将要改变哪些部分,然后着手修改并进行测试。作为第1步的理解代码,往往是最难的。如果别人给你的代码很容易理解,接下来的工作就省心多了。要敬重这个黄金法则,你欠他们一份情,因此也要让你自己的代码简单、便于阅读。

明白地告诉阅读程序的人,代码都做了什么,这是让其便于理解的一种方式。让我们看一些例子。

coffeeShop.PlaceOrder(2);

通过阅读上面的代码,可以大致明白这是要在咖啡店中下一个订单。但是,2到底是什么意思?是意味着要两杯咖啡?要再加两次?还是杯子的大小?要想搞清楚,唯一的方式就是去看方法定义或者文档,因为这段代码没有做到清晰易懂。

所以我们不妨添加一些注释。

coffeeShop.PlaceOrder(2/*large cup*/);现在看起来好一点了,不过请注意,注释有时候是为了帮写得不好的代码补漏(见第105页习惯26:用代码沟通)。

Java5与.NET中有枚举值的概念,我们不妨使用一下。使用C#,我们可以定义一个名为CoffeeCupSize的枚举,如下所示。

public enum CoofeeCupSize

{

Small,

Medium,

Large

}

接下来就可以用它来下单要咖啡了。

coffeeShop.PlaceOrder(CoffeeCupSize.Largxe);这段代码就很明白了,我们是要一个大杯的咖啡。

作为一个开发者,应该时常提醒自己是否有办法让写出的代码更容易理解。下面是另一个例子。

Line 1 public int compute(int val){int result = val<<1;

//…more code…

return result;

但对没有类似背景的人们来说,又会如何—他们能明白吗?也许团队中有一些刚刚转行做开发、没有太多经验的成员。他们会挠头不已,直到把头发抓下来。代码执行效率也许很高,但是缺少明确的意图和表现力。

用位移做乘法,是在对代码进行不必要且危险的性能优化。Result-val*2看起来更加清晰也可以达到目的,而且对于某种给定的编译器来说,可能效率更高(懂得丢弃,见34页习惯7)。不要表现得好像很聪明似的,要遵循PIE原则:代码要清晰的表达意图。

要违反了PIE原则,造成的问题就不只是代码可读性那么简单了——它会影响到代码的正确性。下列代码是一个C#方法,试图同步对CoffeeMaker中MakeCoffee()方法进行调用。

Public void MakeCoffee

{

Lock(this)

{

//…operation

}

}

这个方法的作者想设置一个临界区(critical section)——任何时候最多只能有一个线程来执行操作中的代码,要达到这个目的,作者在CoffeeMaker实例中声明了一个锁。一个线程只有获得这个锁,才能执行这个方法。(在JAVA中,会使用synchronized而不是lock,不过想法是一样的。)对于java或NET程序员来说,这样写顺理成章,但是其中有两个小问题。首先,锁的使用影响范围过大;其次,对一个全局可见的对象使用了锁,我们进一步来看看这两个问题。

假设CoffeeMaker 同时可以提供热水,因为有些人希望早上能够享用一点伯爵红茶。我想同步GetWater()方法,因此调用其中的lock(this)。这会同步任何在CoffeeMaker上使用lock的代码,也就意味着不能同时制作咖啡以及获取热水。这是开发者原本的意图吗?还是锁的影响范围太大了?通过阅读代码并不能明白这一点,使用代码的人也就迷惑不已了。

同时,MakeCoffee()方法的实现在CoffeeMaker对象上声明了一个锁,而应用的其他部分都可以访问CoffeeMaker对象。如果在一个线程中锁定了CoffeeMaker对象实例,然后在另外一个线程中调用那个实例之上的MakeCoffee()方法呢?最好的状况也会执行效率很差,最坏的状况会带来死锁。

让我们在这段代码上应用PIE原则,通过修改让它变得更加明确吧。我们不希望同时有两个或更多的线程来执行MakeCoffee()方法。那为什么不能为这个目的创建一个对象并锁定它呢?

private Object makeCoffeeLock = new Object();public void MakeCoffee()

Lock(makeCoffeeLock)

{

// … operation

}

这段代码解决了上面的两个问题 — 我们通过指定一个外部对象来进行同步操作,而且更加明确地表达了意图。

在编写代码时,应该使用语言特性来提升表现力。使用方法名来传达意向,对方法参数的命名要帮助读者理解背后的想法。异常传达的信息是哪些可能会出现问题,以及如何进行防御式编程,要正确地使用和命名异常。好的编码规范可以让代码变得易于理解,同时减少不必要的注释和文档。

要编写清晰的而不是讨巧的代码。向代码读者明确表明你的意图。可读性差的代码一点都不聪明。

切身感受

应该让自己或团队的其他任何人,可以读懂自己一年前写的代码,而且只读一遍就知道它的运行机制。

平衡的艺术

□ 现在对你显而易见的事情,对别人可能并非如此,对于一年以后的你来说,也不一定显而易见。不妨将代码视作不知道会在未来何时打开的一个时间胶囊。

□ 不要明日复明日。如果现在不做的话,以后你也不会做的。

□ 有意图的编程并不是以为着创建更多的类或者类型。这不是进行过分抽象的理由。

□ 使用符合当时情形的耦合。例如,通过散列表进行松耦合,这种方式适用于在实际状况中就是松耦合的组件。不要使用散列表存储紧密耦合的组件,因为这样没有明确表示出你的意图。