《驯服烂代码》第1章 撒谎的路标与裸奔的代码

来源:互联网 发布:大学生就业难 知乎 编辑:程序博客网 时间:2024/04/27 19:26

此文乃敝人所撰写《驯服烂代码》之第一章,供各位网友试读。多多指点,反馈请发至邮箱wubin28@gmail.com。多谢!——伍斌_Ben, 2013.11.28


第1章 撒谎的路标与裸奔的代码

“什么是软件?”上个世纪90年代初的一个冬日,在北京东南近郊的一所大学里,一位年近花甲的老师,给我们这些计算机系的学生讲软件工程这门课时,问了这个问题。对于那个时候几乎没有多少机会摸电脑的我来说,软件就是学校机房里那些DEC小型机上令人费解的的命令,和286个人电脑里那些好玩的吃豆子和赛车的游戏。 “软件不仅仅是程序,还包括描述程序的文档。软件就是程序加文档。”老师对软件的定义,深深地刻在我的脑子里,其程度之深,在之后的很长一段时间里,总令我觉得文档对于软件的影响力,似乎要盖过程序。

这一点在我大学毕业后二十年的软件开发相关工作的实践中,不断地得到印证。各种各样的文档——需求文档、概要设计文档、详细设计文档、测试文档等等,在我所先后经历的多个软件开发项目中,始终占据着重要的地位。“毕竟,只要文档在,就不怕开发人员的频繁流动。”一位软件开发经理这样对我说。除了要写Word或Excel的文档,程序员们还被要求在源代码中写尽量详尽的注释。在软件开发经理眼中,撰写文档和编写注释的习惯,是衡量程序员是否称职的一项重要标准。

“为保证客观性,软件开发完成后,应该由不同的人来对其进行测试。”老师的这句话也时时萦绕在我的耳边。这句话的影响力是如此巨大,以至于当我在前些年做程序员时,我和周围的程序员们,都一致认为测试就是测试工程师的事情。在十多年中,我经历的每一个项目,都无一例外地有一个独立于开发团队的测试团队,开发团队将代码开发完成后,简单地在自己机器上跑一跑,然后就提交代码并丢给测试团队去测试。

十几年来,不管是开发新功能还是修复bug,我一直在努力地撰写文档,编写和修改代码及注释,然后交给测试人员去测试,再去改测试人员提出的bug,这一切看起来都像教科书上描述的那样地完美和正确。但是我最后却发现,用这种方法开发出来的软件的内在质量,比如可理解性、可测试性、可扩展性,却似乎并没有随着我的上述努力的增多而增强,有时反而会减弱,换句话说,这种方法无法阻止烂代码的滋生。让我们先用这种传统的瀑布式开发方法,操练一个编码招式,来看看其中会有什么问题?

1.1编码招式

编码招式即英文Code Kata的中译,是《程序员修炼之道:从小工到专家》(ThePragmatic Programmer: From Journeyman to Master)一书的合著者、美国程序员Dave Thomas大约在2003年前后创造的字眼,表示一个编码练习,程序员可以通过反复地操练该练习来提高自身的编程技能。Kata是一个日语片假名かた的英译,对应的汉字是“型”或者“形”,表示供单人或双人进行操练的、经过仔细编排的动作模式。[1]

编码招式,说白了其实就是程序员练功时选择的那个编程题目。说起练功,我就能很自然地联想到京剧演员练习压腿和踢腿,相声演员练习绕口令和开声,和习武之人的独自站桩和与人过招切磋。与之相比,程序员的练功似乎就没有那么讲究。从上世纪80年代面向对象的编程语言出现以来至今这30多年的时间里,国内的绝大部分程序员的所谓“练功”,仅仅停留在读一些技术书籍和博客,顶多再照着示例代码写一些程序,运行一下而已。即使在程序员编程水平很高的国外,直到2004年5月,法国程序员Laurent Bossavit才写了一篇有关多位程序员在一起操练一些编码招式的“编码道场”的博客。这里的“编码道场”是英文coding dojo的中译,其中的dojo同样也来自于日语,是片假名どうじょう的英译,对应的汉字是“道场”,指一个正式的训练场所,来供学习日本武术的学生聚在一起进行操练。由于“道场”一词在国内的使用范围较小,且在“编码道场”中主要是进行操练活动,所以本书下文把“编码道场”称作“编码操练”,二者含义相同。编码操练意指多位程序员聚在一起,用两人结对的形式,操练编码招式的过程。编码招式和编码操练在国外已经发展了近10年,其影响力不断扩大,到2009年又出现了编码闭关(code retreat)的新的形式,即几十个程序员聚在一起,用一整天的时间来在编码操练中结对操练编码招式。编码招式、编码操练和编码闭关这几年在国内也陆续得到一些发展,比如在撰写本书时,我受《重构与模式》(Refactoring to Patterns)一书的作者Joshua Kerievsky于1995年在美国纽约创办设计模式学习小组的启发,于2013年4月在北京创办了免费公益的“北京设计模式学习组[2]”,到撰写本书时,已举办10次活动,每次能吸引8~20位程序员来进行结对操练编码技艺。这一切似乎都在表明,编写程序不再仅仅是按照既定的软件架构或框架,来像垒砖那样被动地“填”代码,而是像唱京戏、说相声、练武术那样,更加强调人的创造性,是一门需要反复操练才能悟道出师的手艺。

既然编码招式是供程序员在没有工作压力的情况下练功操练时使用的,为了能够让程序员们在操练时获得更有趣的体验,编码招式需要设计得“有趣”,即除了题目的内容可以是生活中有意思的场景外,最好还能通过实现这个招式,操练一些有挑战性的技能,比如结对编程和设计模式。

对于本书中所有的编码招式,我都将邀请您——我的亲爱的读者——来与我一起进行结对编程。“啊?什么是结对编程?我从来没有尝试过哩!”结对编程其实一点都不神秘,如果把编程比作打网络游戏,结对编程就好比两个人结伴去打魔兽,除了可以相互学习切磋之外,还能相互有个照应。好了,一起来看看本书的第一个编码招式。

1.2 酒店世界时钟招式

这个招式是我在2013年9月,为在“北京设计模式学习组”的第9次活动中操练Observer设计模式而编写的。灵感来自于我在酒店下榻时,在大堂里看到的那些墙壁上悬挂的显示世界上各个主要城市的时间的时钟。我在想,如果这些时钟都走时不准,一个个地分别调时间太麻烦,要是能够只调准一个城市的时钟,其余城市的时钟都能相应地自动调准,那该多好。

比方说在一家酒店的大堂里,有5个时钟,分别显示北京、伦敦、莫斯科、悉尼和纽约的时间。其中,伦敦与UTC(CoordinatedUniversal Time,协调世界时)时间[3]保持一致,北京比UTC时间早8小时,莫斯科比UTC时间早4小时,悉尼比UTC时间早10小时,纽约比UTC时间晚5小时。若某个城市的时钟走时不准,需要调整时间时,只需调准该城市的时间,其余4个城市的时间能够相应地自动调整准确。

在程序员中,熟悉Java语言的人数相对较多。那么咱们能不能用Java语言,实现上面这个编码招式呢?

“好吧,需求已经说得很清楚了。正好前段时间我刚刚读完一本讲ICONIX过程[4]的书,正好可以用这个编码招式来操练一下UML和用例驱动的对象建模。”你说。

“不错!编码操练的一大特色就是可以用来实践自己学到的任何新东西。咱们可以试试看。”

1.3 需求描述

“首先把功能性需求整理成下面这样的需求列表,并编上号。”你说。

1)REQ01:酒店的大堂里,有5个时钟,分别显示北京、伦敦、莫斯科、悉尼和纽约的时间。

2)REQ02:伦敦与UTC时间保持一致,北京比UTC时间早8小时,莫斯科比UTC时间早4小时,悉尼比UTC时间早10小时,纽约比UTC时间晚5小时。

3)REQ03:若某个城市的时钟走时不准,需要调整时间时,只需调准该城市的时间,其余4个城市的时间能够相应地自动调整准确。

1.4 领域模型

“领域模型定义了‘系统能够做什么’的功能需求,重在解决沟通误解的问题,它关注项目中所有概念的‘准确性’,需要建立描述问题领域的通用词汇表,来消除误解和增强概念的准确性。这个领域模型词汇表随着项目的进展总是要不断地完善和更新。”你说。

“噢,听起来不错,那么设计领域模型的第一步需要做什么呢?”我问。

“找出领域类。从上面那个需求列表里找出一些重要的名词,可以作为初步的领域类。”你说。

“嗯,依我看重要的名词有:北京钟、伦敦钟、莫斯科钟、悉尼钟、纽约钟、UTC时间。或许还应该有酒店员工。”我说。

“对。先用这些名词,以后再继续调整。下一步咱们可以创建词汇表,来描述这些名词。”你说。

这样就有了如表1-1所示的词汇表。

表1-1       词汇表

中文词条

英文词条

含义

北京钟

BeijingClock

酒店大堂中显示北京时间的钟

伦敦钟

LondonClock

酒店大堂中显示伦敦时间的钟

莫斯科钟

MoscowClock

酒店大堂中显示莫斯科时间的钟

悉尼钟

SydneyClock

酒店大堂中显示悉尼时间的钟

纽约钟

NewYorkClock

酒店大堂中显示纽约时间的钟

UTC时间

UtcTime

Coordinated Universal Time,协调世界时,是全世界用于调整时钟和时间的主要时间标准

酒店员工

HotelEmployee

在大堂中调整城市时钟的酒店员工

“下一步就可以画领域模型类图了。为了让领域模型类图更有条理,可以从这5个城市的时钟抽象出一个‘城市时钟’类。现在可以先在词汇表中添加一行,表示‘城市时钟’。”你说。

在词汇表中添加的那一行如表1-2所示。

表1-2 词汇表中增加的一行

中文词条

英文词条

含义

城市时钟

CityClock

从各个城市的时钟中抽象出来的类

“这5个城市的时钟都继承这个‘城市时钟’类,是泛化关系。而‘城市时钟’类中又包含一个UTC时间类,是聚合关系。”你说。


领域模型类图如图1-1所示。

 


图1- 1 领域模型类图

“接下来,可以画一个用户界面草图。再结合前面的需求列表、词汇表、领域模型类图,就可以进行Use Case用例模型的分析,来定义用户与系统之间该如何交互。最后再绘制健壮图,进行健壮图分析,引入边界对象、实体对象和控制器这三个对象,这样咱们这个招式的对象就设计好了。”你说。

“什么是边界对象和实体对象?”我问。

“边界对象是系统与外部世界的接口,比如用户界面或网页,相当于MVC设计模式中的View。实体对象是领域模型中定义的领域类。”你说。

“哦,咱们能不能先不考虑这个编码招式的用户界面?直接使用领域模型类图开始编程了呢?”我说。

“可以。用户界面设计起来也挺麻烦的,为这个小小的编码招式也犯不上。等这些类都编写完了,最后再写一个main方法来调用一下这些类,测一测就好了。不过Use Case用例图还是画一下吧,工作单位一直严格要求我们程序员写这类文档,还是操练一下吧。”你说。

用例图如图1-2所示。

 

 

图1- 2用例图


“首先,酒店员工这个角色与‘Update the time of oneclock’这个用例打交道,来更新一个时钟的时间。然后这个用例会调用‘Update the time of all other clocks automatically’这个用例,表示自动更新其余城市时钟的时间。”你说。

1.5 使用设计模式

“等等。咱们不妨看看这个招式的场景是否有设计模式可以适用,这样可以借鉴前人的经验,而不用自己闭门造车了。”我说。

“嗯,好主意!可以快速浏览一下四巨头的23个设计模式[5]的意图……嗯,看起来Observer观察者模式的意图正好和咱们的编码招式相吻合。‘定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并被自动更新。’这不正好是调整一个城市时钟的时间,其余城市的时钟都能自动更新时间吗。这样咱们可以把上面的领域模型类图照着四巨头画的类图改一改。”你说。

在四巨头的《设计模式》一书中Observer模式的UML类图如图1-3所示。

 

 

图1- 3 四巨头书中的Observer模式的类图

更新后的领域模型类图如图1-4所示。

 

 

图1- 4 更新后的领域模型类图


1.6 详细设计

“现在类图有了,下面可以参考四巨头的类图来细化咱们的类图,在每个类上添加暴露给外界的接口,也就是公共方法。”你说。

细化后的类图如图1-5所示。

 

 

图1- 5 细化后的类图


你开始为我解释这张细化后的类图。

“为简化起见,对于时间咱们只考虑小时,所以时间都用int类型来表示。”

“在细化后的类图中,TimeSubject类可以用一个名叫cityClocks的HashMap来保存所有5个城市的CityClock类的对象。为了便于向HashMap中添加或从其中移除城市的对象,需要有attach()和detach()这两个方法。TimeSubject类的notify()方法所要做的事情在图中用一个备注框标出来了,即对于TimeSubject类的cityClocks这个HashMap成员变量,用一个for循环来调用其中保存的每一个CityClock对象中的updateCityTime()方法,来将所有城市的时间进行自动更新。而这个notify()方法,可以通过UtcTime的setUtcTime()方法来触发调用。”

“CityClock类有一个私有的成员变量cityTime,用于保存各个城市当地的时间;它还有一个成员变量utcTime,用来保存一个指向UtcTime类的引用,以便于从UtcTime中取出UTC时间,来计算自己所表示的城市的当地时间cityTime。它的每一个子类,都有一个UTC_OFFSET的私有静态成员变量,来保存每个城市相对于UTC时间的时差。这5个子类所继承的那个updateCityTime()方法由TimeSubject类的notify()方法来调用,刚才已经说过了,而这5个子类所继承的另一个方法setUtcTime(),是用来调用UtcTime类的setUtcTime()方法的,进而能够触发调用TimeSubject类的notify()方法,从而能够实现更新一个城市的时间,就能自动更新所有城市的时间。”

“UtcTime类扩展了其父类TimeSubject,且有一个utcTime私有成员变量,用来保存UTC时间。”你说。

“很好!现在可以编码了吧。”我说。

1.7 编码实现

对于Java编程来说,Maven是个好工具。只要运行下面一行命令,Maven就能为你创建一个空的Java项目,并随时准备为你编译打包,你只要用像IntelliJ这样的IDE打开pom.xml文件,一门心思写Java代码就好了。

下面就是那行用于创建一个空的Java项目的Maven命令。

mvn archetype:generate-DgroupId=com.wubinben.tamingbadcode.waterfall.hotelworldclock -DartifactId=tbc-DarchetypeArtifactId=maven-archetype-quickstart -Dversion=1.0

“现在,空项目已经用Maven建好了,github也配好了。咱们按照细化后的类图来编写第一个类TimeSubject[6]。”你说。

下面就是TimeSubject类的代码:

public abstract class TimeSubject {

    protected staticHashMap<String, CityClock> cityClocks = new HashMap<String,CityClock>();

 

    public static voidattach(String cityName, CityClock cityClock) {

      cityClocks.put(cityName,cityClock);

    }

 

    public static voiddetach(String cityName) {

      cityClocks.remove(cityName);

    }

 

    public abstract voidnotifyAllCityClocks();

}

“如果按照类图来实现,由于这个类是个抽象类,没法实例化,所以我就把成员变量cityClocks和attach()与detach()这两个成员方法都搞成静态的。成员方法notify()是抽象的,留给它的子类来实现。哦,方法名notify()已经被Object类给占用了,notifyAll()也被占用了,所以只好把notify()改名叫notifyAllCityClocks()了。”你说。

“现在CityClock下面有红线,表示这个类还没有定义。咱们现在写这个类吧。在IntelliJ里,你可以把光标移到CityClock中,然后敲Alt+Enter键,就能让IntelliJ自动帮你写这个类。”我说。

“哦,这么方便!你要是不说,我还要傻乎乎地一点点地写呢。”你说。

在IntelliJ的帮助下,按照类图写出的CityClock类如下所示:

public abstract class CityClock {

    protected int cityTime;

    protected UtcTime utcTime;

 

    public abstract voidsetUtcZeroTime(int utcZeroTime);

    public abstract voidupdateCityTime(int utcZeroTime);

}

“呃,utcTime这个名字真的让我有点纠结,它有两个含义,既可以指UtcTime这个类的一个对象,也可以指UtcTime这个类中的用来保存UTC时间的那个成员变量。为了区分,我把后者改名叫utcZeroTime,表示与UTC时间的时差为0的时间。所以CityClock类中抽象方法setUtcTime()改名成为setUtcZeroTime(),它和抽象方法updateCityTime()的输入参数也改名为utcZeroTime了。”你说。

“嗯,把那个类图的文件传给我一份。这样你一边写代码,我一边在我的电脑上修改那个类图。”我说。

“好的。用QQ传给你吧。CityClock类中的UtcTime下面标出了红线,咱们该创建这个类了。还是用Alt+Enter来帮我创建,这个快捷键真是太好使了!”你说。

创建出的UtcTime类如下所示:

public class UtcTime extends TimeSubject {

    private int utcZeroTime;

 

    @Override

    public voidnotifyAllCityClocks() {

      Iterator<CityClock>cityClockIterator = cityClocks.values().iterator();

      while(cityClockIterator.hasNext()) {

          CityClock cityClock = cityClockIterator.next();

          cityClock.updateCityTime(utcZeroTime);

      }

    }

 

    public intgetUtcZeroTime() {

      return utcZeroTime;

    }

 

    public voidsetUtcZeroTime(int utcZeroTime) {

      this.utcZeroTime =utcZeroTime;

      notifyAllCityClocks();

    }

 

}

“在UtcTime类里,成员变量utcTime改名为utcZeroTime了。notifyAllCityClocks()方法里有一个循环,更新所有城市的时钟的时间。setUtcZeroTime()方法里会调用notifyAllCityClocks()方法。”你说。

“嗯。接下来做什么?”我问。

“接下来,该一个一个地实现那5个城市的时钟类了。先从BeijingClock开始。”你说。

BeijingClock类的代码如下所示:

public class BeijingClock extends CityClock {

    private static final intUTC_OFFSET = 8;

 

    @Override

    public voidupdateCityTime(int utcZeroTime) {

        cityTime = utcZeroTime+ UTC_OFFSET;

    }

}

“BeijingClock类实现了父类中抽象的updateCityTime()方法,来把UTC时间加上北京与UTC时间之间的时差UTC_OFFSET,就能得到北京时间。本来我还想继续在BeijingClock类中实现父类中抽象的setUtcZeroTime()方法,但我发现这个方法的实现完全可以放到父类中,从而可以让它的5个子类复用。所以我改了一下父类CityClock,把抽象的setUtcZeroTime()方法改成一个已经实现了的方法。”你说。

下面就是CityClock类中被修改了的那部分代码,其中带有“-”号的行表示删除的行,带有“+”号的行表示添加的行,下面的代码表示用后面3个带有“+”号的行替换了第1个带有“-”号的行。

-    public abstract voidsetUtcZeroTime(int utcZeroTime);

+    public voidsetUtcZeroTime(int utcZeroTime) {

+       utcTime.setUtcZeroTime(utcZeroTime);

+    }

“写好了北京时钟类,剩下4个城市的时钟类就可统统照此办理,它们之间主要的区别就是每个城市与UTC时间的时差不同罢了。”你说。

“很好。按照类图,该写的代码都写完了。下一步就是该如何测试这些代码了。”我说。

1.8 测试一下

“要是这个招式能有个用户界面就可以测试了。不过咱们可以写一个main()方法,调用一下这些代码就可以当做测试了。”你说。

“嗯。你把代码都实现了,辛苦了。现在交换一下。我来编写这个main()方法。”我说。

下面就是一个包含有main()方法的Main类的代码:

public class Main {

    public static voidmain(String[] args) {

        // Attach 5 cities toTimeSubject class

        TimeSubject.attach("Beijing",new BeijingClock());

       TimeSubject.attach("London", new LondonClock());

       TimeSubject.attach("Moscow", new MoscowClock());

       TimeSubject.attach("Sydney", new SydneyClock());

       TimeSubject.attach("NewYork", new NewYorkClock());

 

        // Adjust the time ofBeijing clock to be 9

       TimeSubject.getCityClock("Beijing").setUtcZeroTime(9);

 

        // Display the time ofthe 5 cities

       System.out.println("The time of Beijing is " + TimeSubject.getCityClock("Beijing").getCityTime());

       System.out.println("The time of London is " + TimeSubject.getCityClock("London").getCityTime());

       System.out.println("The time of Moscow is " + TimeSubject.getCityClock("Moscow").getCityTime());

        System.out.println("Thetime of Sydney is " + TimeSubject.getCityClock("Sydney").getCityTime());

       System.out.println("The time of New York is " + TimeSubject.getCityClock("NewYork").getCityTime());

    }

}

“这个main()方法有3步工作需要做。第1步,利用TimeSubject类的attach()方法,把5个城市的对象都添加到该类中那个静态的HashMap成员变量里。第2步,把北京时钟的时间调整到9点,第3步,把这5个城市的时间都打印出来,看看结果。第1步没有问题,不过第2步和第3步中要用到的两个接口,在目前已经编写的代码中没有提供。一个没有提供的接口是第2步中的TimeSubject类的getCityClock()这个方法,因为北京时钟对象是保存在TimeSubject类中的,所以最好TimeSubject类能提供一个getCityClock(String cityName)方法作为接口,该方法接受城市名称作为输入参数,然后返回城市名称所对应的城市时钟对象;另一个没有提供的接口是第3步中的CityClock类的getCityTime()方法,我需要获取某个城市时钟的当地时间来打印出来。即使现在没有这两个接口,我也在这里把实际使用它们的调用代码在main()方法里给先写出来。”我说。

“这与我的习惯正好相反。我还是习惯先设计并实现好一个接口,然后再使用它。比如我会先在TimeSubject类中添加getCityClock()方法,然后再回来写main()方法来调用它。”你说。

“嗯,以前我也和你一样。不过我后来发现Alt+Enter真是太好使了。先写你期望但尚不存在的接口,然后让IntelliJ来帮你实现接口代码,比你自己一点点写接口要快多了。”我说。

“哦,这倒是。”你说。

下面就是在TimeSubject类中添加getCityClock()方法的代码。

+    public static CityClockgetCityClock(String cityName) {

+        if(cityClocks.keySet().contains(cityName)) {

+            returncityClocks.get(cityName);

+        }

+        throw newIllegalStateException("The city name " + cityName + " does notexist.");

+    }

“接下来用Alt+Enter快捷键来在CityClock类中实现getCityTime()方法。”我说。

下面是在CityClock类中的添加的getCityTime()方法的代码:

+    public int getCityTime(){

+        return cityTime;

+    }

“现在Main类的源代码里面已经没有红色的编译出错代码了。咱们可以运行一下main()方法。呃……空指针错!”我说。

出错信息如下所示:

Exception in thread "main" java.lang.NullPointerException

      atcom.wubinben.tamingbadcode.waterfall.hotelworldclock.CityClock.setUtcZeroTime(CityClock.java:15)

      atcom.wubinben.tamingbadcode.waterfall.hotelworldclock.Main.main(Main.java:20)

“在CityClock类文件的第15行,是调用成员变量utcTime所引用的一个UtcTime类的对象的setUtcZeroTime()方法,来设置UtcTime类的UTC时间的。哦,这里的成员变量utcTime没有初始化,所以是空值。给它初始化就好了。”我说。

CityClock类的成员变量utcTime的代码修改如下:

-    protected UtcTimeutcTime;

+    protected UtcTime utcTime= new UtcTime();

“现在再运行一下main()方法。噢耶!终于出来结果啦!可是……结果好像不大对。”你说。

运行main()方法的结果如下:

The time of Beijing is 17

The time of London is 9

The time of Moscow is 13

The time of Sydney is 19

The time of New York is 4

“嗯,北京时间开始时被调整到9点,可结果显示出来却是17点。看来时间设置有问题。要不先看看把北京时钟设置为9点的CityClock类的setUtcZeroTime()方法是如何实现的。”你说。

main()方法中将北京时钟设置为9点的那行代码如下:

       TimeSubject.getCityClock("Beijing").setUtcZeroTime(9);

上述代码所调用的CityClock类的setUtcZeroTime()方法代码如下:

    public voidsetUtcZeroTime(int utcZeroTime) {

       utcTime.setUtcZeroTime(utcZeroTime);

    }

“啊!这里有问题!setUtcZeroTime()方法的输入参数期望得到UTC时间,但是实际传给它的参数却是北京时间9点,根本就不是UTC时间。”我说。

“那就先把setUtcZeroTime()方法的输入参数名改为cityTime,与实际传入的参数含义保持一致,然后再把这个当地时间cityTime转换为UTC时间。”你说。

“好的。改名字用IntelliJ提供的重构工具很方便。嗯,把当地时间转换为UTC时间这件事应该放到哪里去做呢?因为这种转换需要时差UTC_OFFSET,而那5个城市时钟类具有UTC_OFFSET,所以这个工作由5个城市时钟类来完成,而在它们的父类中声明一个抽象方法来规范一下这项工作。这个抽象方法的名字可以叫convertCityTimeToUtcZeroTime(),它把当地时间转换为UTC时间。”我说。

在CityClock类中更改setUtcZeroTime()方法的参数名并添加一抽象方法的代码如下:

-    public voidsetUtcZeroTime(int utcZeroTime) {

-       utcTime.setUtcZeroTime(utcZeroTime);

+    public voidsetUtcZeroTime(int cityTime) {

+       utcTime.setUtcZeroTime(convertCityTimeToUtcZeroTime(cityTime));

     }

 

+    protected abstract intconvertCityTimeToUtcZeroTime(int cityTime);

“既然在父类CityClock里添加了一个抽象方法,那么那5个子类需要实现这个抽象方法。一个一个用Alt+Enter来帮我实现吧。”我说。

在BeijingClock类中添加的实现上述抽象方法的代码如下(其他城市时钟的代码与之相同):

+    protected intconvertCityTimeToUtcZeroTime(int cityTime) {

+        return cityTime -UTC_OFFSET;

+    }

+

+    @Override

“好了,可以再次运行main()方法了。耶!这次北京时间是9点了!”我说。

再次运行main()方法的输出结果如下:

The time of Beijing is 9

The time of London is 1

The time of Moscow is 5

The time of Sydney is 11

The time of New York is -4

“对。不过最后一个城市纽约的时间怎么是-4点?”你说。

“哦,咱们为了省事,用整数来代表小时,如果与时差UTC_OFFSET进行运算时,肯定会出现0到24这个范围之外的数字。咱们可以再编写一个方法,来让所有与时差UTC_OFFSET进行运算的结果都回到0到24这个范围里面。而这个方法可以放到CityClock父类中供各个子类复用。这个方法可以叫做keepInRange0To24()。”我说。

BeijingClock类中添加keepInRange0To24()方法的调用如下(其他4个城市时钟类与之相同):

-        return cityTime -UTC_OFFSET;

+        returnkeepInRange0To24(cityTime - UTC_OFFSET);

-        cityTime =utcZeroTime + UTC_OFFSET;

+        cityTime =keepInRange0To24(utcZeroTime + UTC_OFFSET);

CityClock类中添加的keepInRange0To24()方法代码如下:

+    protected int keepInRange0To24(inthour) {

+        if (hour < 0) {

+            return hour + 24;

+        }

+        if (hour > 24) {

+            return hour - 24;

+        }

+        return hour;

+    }

“再运行一下main()方法。这次全对了!”我说。

再次运行main()方法的结果如下:

The time of Beijing is 9

The time of London is 1

The time of Moscow is 5

The time of Sydney is 11

The time of New York is 20

1.9 暴露的问题

“这个招式终于可以告一段落了。哎,对了,咱们对设计做的所有的修改,你都更新到那个类图文件里了吗?”你问。

“噢!我光顾着看你的代码,早把这茬儿给忘了!要不你根据最新的代码再更新一下类图?”我说。

“哦,回头我会更新。不过咱们的代码还有很多地方需要进一步修改,比如我早就看着TimeSubject这个类里面那些静态成员变量和成员方法不顺眼,这和全局变量没啥区别,将来或许是一些难缠的问题的根源,最好能把静态给去掉。不过这样一来,那个HashMap的成员变量就得下移到其子类UtcTime中,如果这样的话,TimeSubject就似乎没有存在的必要。对了,当初为什么要有TimeSubject类呢?都是照搬四巨头的类图惹的祸。如果真要这么改,那个类图又得有不少需要更新的地方。”你说。

“是呀。因为代码是程序员写的,所以维护类图与代码一致这项工作,只能由程序员自己来做。但在项目进度的压力下,有几个程序员能坚持这样繁重的维护工作?就好比秋天清扫落叶,刚一扫完,就会落下新的落叶,无穷无尽。像过时的类图这样的文档,随着时间的推移,会越来越离谱,最终会变成一个撒谎的路标,其效果还不如没有路标,因为代码从不会撒谎。”我说。

“我倒是能够通过一些工具来把代码转换为UML类图。不过转换过来的类图过段时间还得过时。这个问题真有点难办呀!”你说。

“另外,咱们这个招式发现的那几个问题,比如空指针、时间不准、超出范围等问题,都在什么时候发现的?”我问。

“都在咱们写了main()方法并运行后发现的。但这正是main()方法测试的意义呀。”你说。

“对。但是你想过没有,你完成了这一招式的功能,把main()方法连同其他源代码提交到版本控制系统后,再开发其他功能的时,你还会想起来运行这个招式的main()方法吗?如果你在开发其他功能的时候,修改了咱们这个招式的代码,你又没有运行它的main()方法,那效果不就和你在写main()方法前所处的情形是一样的吗?你可能会引入新的bug。咱们这个招式的main()方法所做的测试工作,就像你的外衣一样,起到了保护的左右。这个测试需要频繁地运行。而没有频繁运行的测试来保护的代码,就是裸奔的代码。”我说。

“撒谎的路标?裸奔的代码?嗯,有点意思……那么,怎样才能解决这两个问题呢?”

下一章“将规格与测试合二为一”或许能够解决这两个问题。

 

 



[1] 参见:http://en.wikipedia.org/wiki/Kata

[2] 参见北京设计模式学习组的网站:http://www.bjdp.org/

[3] UTC时间,是全世界用于调整时钟和时间的主要时间标准。

[4] ICONIX是一种软件开发方法,它的产生时间要早于Rational统一过程(RUP)、极限编程(XP)和敏捷软件开发。与RUP类似,ICONIX过程是被UML用例来驱动的,但ICONIX比RUP更加轻量级。与XP和敏捷方法不同,ICONIX能够提供足够的需求和设计文档,但不会因过度分析而导致项目难以进行。ICONIX过程只需在4个步骤中,使用4个UML图,就能把用例文本转变成可工作的代码。详见http://en.wikipedia.org/wiki/ICONIX。

[5] 参见Erich Gamma、Richard Helm、Ralph Johnson和John Vlissides撰写的《设计模式》一书。

[6] 本章代码可在以下链接下载:https://github.com/wubin28/BookTamingBadCode