第1章 刻舟求剑的文档
“什么是软件?”20世纪90年代初的一个冬日,在北京东南部的一所大学里,一位年近花甲的老师,给我们这些计算机系的学生讲软件工程这门课时,问了这个问题。对于当时几乎没有机会接触计算机的我来说,软件就是学校计算机房里那些DEC小型机上令人费解的命令,和286个人计算机里那些好玩的“吃豆子”和“赛车”游戏。“软件不仅仅是程序,还包括描述程序的文档。软件就是程序加文档。”老师对软件的定义,深深地刻在我的脑子里,其程度之深,以至于在之后的很长一段时间里,总令我觉得文档对于软件的影响力,似乎要超过程序。
这一点在我大学毕业后20年的软件开发相关工作的实践中,不断地得到印证。各种各样的文档——需求文档、概要设计文档、详细设计文档、测试文档等,在我先后经历的多个软件开发项目中,始终占据着重要的地位。“毕竟,只要文档在,就不怕开发人员的频繁更换。”一位软件开发经理这样对我说。除了要写Word或Excel的文档,程序员们还被要求在源代码中写尽量详尽的注释。在软件开发经理眼中,撰写文档和编写注释的能力,是衡量程序员是否称职的一项重要标准。
“为保证客观性,软件开发完成后,应该由不同的人员来对其进行测试。”老师的这句话也时时萦绕在我的耳边。这句话的影响力是如此巨大,以至于前些年我做程序员时,我和周围的程序员们都一致认为测试就是测试工程师的事情。在10多年中,我经历的每一个项目,都无一例外地有一个独立于开发团队的测试团队,开发团队将代码开发完成后,简单地在自己机器上运行一下,然后就将代码提交给测试团队去测试了。
十几年来,不管是开发新功能还是修复bug,我一直在努力地撰写文档,编写和修改代码及注释,然后交给测试人员去测试,再去修改测试人员提出的bug,这一切看起来都像教科书上描述的那样地完美和正确。但是最后我却难过地发现,用这种方法开发出来的软件,无一例外逐渐沦为烂代码。在烂代码的沼泽里,即使有文档,也读不懂代码;即使bug很小,也不敢修改代码。我甚至怀疑,这种方法或许会助长烂代码的滋生。这些文档就好像“刻舟求剑”故事中那个刻在船身上的记号。让我们先用这种传统的测试后行(test last)的开发方法,做一个编程操练吧,来看看其中会有什么问题。
对于本书中所有的编程操练,我都将邀请您——我的亲爱的读者——来与我一起结对编程。
“啊?和我结对编程?我还从来没试过哩!”读者可能会说。
结对编程其实一点都不神秘,如果把编程比作打网络游戏,结对编程就好比两个人结伴去打魔兽,除了可以相互学习切磋之外,还能相互有个照应。一起来看看本书的第一个编程操练。
这个操练是我于2013年9月为在“北京设计模式学习组”的第9次活动中操练Observer设计模式而编写的。灵感来自于我在酒店下榻时,在大堂里看到的墙壁上悬挂的那些显示世界上各个主要城市的时间的时钟。我在想,如果所有这些时钟都走时不准,酒店大堂服务员一个个地分别调时间太麻烦,而这位服务员的智能手机上肯定用的是该酒店所在地的当地时间,要是能够在调准服务员手机时间的同时,将酒店大堂墙壁上所有城市的时钟根据时差相应地自动调准,那该多方便。
假如在北京一家酒店的大堂里有5个时钟,分别显示北京、伦敦、莫斯科、悉尼和纽约的时间。其中,伦敦与UTC(Coordinated Universal Time,协调世界时)时间一致,北京比UTC时间早8小时,莫斯科比UTC时间早4小时,悉尼比UTC时间早10小时,纽约比UTC时间晚5小时。若所有这些城市的时钟都多少有些走时不准,需要调整时间时,大堂服务员只需调准自己手机上的北京时间,那么墙壁上那5个城市的时间就能相应地自动调整准确。酒店世界时钟和服务员的手机时钟如图1-1所示。
在程序员中,熟悉Java语言的人数相对较多。那么咱们能不能用Java语言实现上面这个编程操练呢?
“好吧,需求已经说得很清楚了。咱们先用UML和Use Case来对这个需求进行分析和设计吧。”
首先把功能性需求整理成下面这样的需求列表,并编上号。
1)REQ01:一家北京的酒店大堂里有5个时钟,分别显示北京、伦敦、莫斯科、悉尼和纽约的时间。
2)REQ02:伦敦与UTC时间一致,北京比UTC时间早8小时,莫斯科比UTC时间早4小时,悉尼比UTC时间早10小时,纽约比UTC时间晚5小时。
3)REQ03:将酒店大堂服务员的智能手机时间设置为北京时间。
4)REQ04:若大堂墙壁上所有那些城市的时钟都或多或少有些走时不准,需要调整时间时,只需调准服务员手机的时间,那么墙上5个城市的时钟时间都能够相应地自动调整准确。
把需求编上号,将来实现和测试这些需求时就好跟踪了。
领域模型定义“系统能够做什么”这样的功能需求,重在解决沟通误解的问题。它关注项目中所有概念的“准确性”,需要建立描述问题领域的通用词汇表,来消除误解和增强概念的准确性。这个词汇表会随着项目的进展,不断地完善和更新。
设计领域模型的第一步是找出领域类。从上面那个需求列表里找出一些重要的名词,可以作为初步的领域类。
这个题目重要的名词有:城市时钟、手机时钟和UTC时间,或许还应该有酒店服务员。
先用这些名词,以后再继续调整。下一步可以创建词汇表,来描述这些名词。
词汇表如表1-1所示。
下一步就可以画领域模型类图了。为了让领域模型类图更有条理,可以从城市时钟和手机时钟里抽象出一个“时钟”类。现在可以先在词汇表中添加一行,表示“时钟”。
在词汇表中添加的那一行如表1-2所示。
城市时钟和手机时钟都继承这个时钟类,是泛化关系。而时钟类中又包含一个UTC时间类,是聚合关系。
领域模型类图如图1-2所示。
首先,酒店服务员这个角色与Update the time of the phone clock这个用例打交道,来更新手机时钟的时间。然后这个用例会调用Update the time of all city clocks这个用例,表示自动更新所有城市时钟的时间。
“咱们不妨看看这个操练的场景是否有设计模式可以适用,这样可以借鉴前人的经验,而不用自己闭门造车。”
“可以快速浏览一下‘四巨头’的23个设计模式的意图。看起来Observer观察者模式的意图正好和咱们的编程操练相吻合。‘定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并被自动更新。’这不正好能满足当调整手机时钟的时间,所有城市的时钟都能自动更新时间这个需求吗?咱们可以把上面的领域模型类图照着‘四巨头’画的类图改一改。”
在“四巨头”的《设计模式》一书中,Observer模式的UML类图如图1-4所示。
类图有了,下面可以参考“四巨头”的类图来细化咱们的类图。在每个类上添加暴露给外界的接口,也就是公共方法。
细化后的类图如图1-6所示。
为简化起见,对于时间这里只考虑小时,所以时间都用int类型来表示。
在细化后的类图中,TimeSubject类可以用一个名叫clocks的HashMap来保存所有5个城市的Clock类的对象和手机时钟对象。为了便于向这个HashMap中添加或删除对象,需要有attach()和detach()这两个方法。TimeSubject类的notify()方法是个抽象方法,它在其子类UtcTime中被实现,具体实现的伪代码在图中用一个注解框标出来了,即对于clocks这个HashMap成员变量,用一个for循环来调用其中保存的每一个Clock对象中的setLocalTime()方法,来对所有时钟的当地时间进行自动更新。而这个notify()方法,可以通过UtcTime类的setUtcTime()方法来触发调用。
Clock抽象类有一个私有的成员变量localTime,用于保存这个时钟所表示的当地时间。它还有另一个私有的成员变量UTC_OFFSET,用于保存它的每一个子类的实例相对于UTC时间的时差。Clock类有一个抽象方法setLocalTime(),用来设置该时钟的当地时间。这个方法之所以是抽象的,是因为Clock类的两个子类——表示手机时钟的PhoneClock类和表示城市时钟的CityClock类——用不同的方式实现了这个方法。在CityClock类中,这个方法的实现仅仅是把传入的参数赋值到其成员变量中;而在PhoneClock类中,这个方法的实现除了赋值外,还调用了PhoneClock类中UtcTime类型的成员变量utcTime的setUtcTime()方法,来触发调用utcTime的notify()方法,从而能够实现自动更新所有城市的时间。
UtcTime类扩展了其父类TimeSubject,且有一个utcTime私有成员变量,用来保存UTC时间。
现在设计文档有了。在开始编程之前,咱们先回顾一下这一章所做的工作:
1)使用了设计驱动的开发方法,来进行有关酒店世界时钟的结对编程操练。
2)整理出了需求列表,并编上了号。
3)创建了领域词汇表,从中找出领域类。
4)画出了领域模型类图。
5)画出了Use Case用例图。
6)根据Observer观察者设计模式,更新了领域模型类图,并画出了细化后的类图。