《驯服烂代码:在编程操练中悟道》一第2章 按图索骥地编写代码

第2章 按图索骥地编写代码

现在,设计文档都齐备了,github也配好了,安装了JDK7和Maven,空项目已经用Maven建好了。还安装好了一个免费使用的IntelliJ IDEA 13.1 Community版,用来编程。现在就可以按照细化后的类图来编写第一个类TimeSubject了。
下面就是TimeSubject类的代码:

public abstract class TimeSubject {
    protected Map<String, Clock> clocks = new HashMap<String, Clock>();

    public void attach(String cityName, Clock clock) {
        clocks.put(cityName, clock);
    }

    public void detach(String cityName) {
        clocks.remove(cityName);
    }

    public abstract void notifyAllClocks();
}

“我有个疑问。这段代码中,TimeSubject类依赖Clock类,而后者还没有创建,您就开始用它编程了。为什么不先编写Clock类呢?”
嗯,好问题!先编写Clock类当然可以。不过先编写TimeSubject类会有额外的好处,就是能让IDEA帮助咱们创建Clock类。后面会看到。
如果按照类图来实现,抽象的成员方法的名字notify()已经被Java语言本身的Object类给占用了,notifyAll()也被占用了,所以只好把notify()改名为notifyAllClocks()了。
现在代码中Clock显示为红色,表示这个类还没有定义。不过现在就可以提交代码到git。
“啊?代码编译还未通过就提交?我们公司可是要求我们直到测试运行通过才能提交代码的。”
对,你们公司说得没错,不过我认为这个要求是针对某种特殊情况而言的,即版本管理系统的代码库是使用客户端-服务器这种集中式管理的情况。你们公司管理代码版本用的是什么工具?
“SVN。”
嗯,SVN就是用这种集中式管理的方式来管理代码版本的。早先的代码管理工具CVS也是用这种方式。这种方式最明显的特点就是一旦断网就无法提交代码。
“是呀,用SVN管理代码必须联网。我在家办公的时候,要是连不上公司网络,那就没法写代码了。”
现在咱们使用的是git,这是一种分布式的代码版本管理工具。用这种分布式的工具提交代码时,代码仅仅是被提交到使用git的这台计算机的本地代码库中,尚未提交到远程的代码库中。所以即使提交尚未通过编译的代码到本地,也不会影响在远程的代码库上进行的编译工作。等咱们一次次提交到本地的代码最后编译运行通过了,再统一push到远程代码库也不迟。
在提交代码之前,先填写Commit Message提交注解。
“哦,我以前一直都不填Commit Message。”
每次提交代码都需要填Commit Message。因为如果想在写错代码时能回退到写错前的代码状态,就得依靠它。另外Commit Message还能起到代码注释的作用。
如果能做到当有少量代码改动时就频繁地把代码提交到本地代码库而不管是否通过编译,且每次提交都能填写有关此次代码改动的意图明确的Commit Message,那么这种每次少量且意图描述清晰的代码提交,一方面增强了将来阅读代码变动的可读性,另一方面当代码写错需要回退时也能有助于做到更精细的回退。
这次提交的Commit Message不妨写成Created and wrote class TimeSubject according to the class diagram.
代码提交完,现在就可以创建那个标红的Clock类了。在IDEA里,可以把光标移到Clock中,然后按Alt+Enter快捷键,就能让IDEA自动帮咱们写这个类了。
Clock类的3处编译错误在图2-1中用箭头标了出来,图中还显示了在Clock上按Alt+Enter快捷键后出现的创建Clock类的快捷菜单。

“哦,这么方便!您要是不说,我还要傻乎乎地一点点地写呢。”
IDEA所创建的Clock类的代码如下所示(CM: Created class Clock.):

public class Clock {
}

按照类图写出的Clock类如下所示(CM: Wrote class Clock according to the class diagram.):

-public class Clock {
+public abstract class Clock {
+    private final int UTC_OFFSET = 0;
+    private int localTime = 0;
+
+    public abstract void setLocalTime(int localTime);
 }

上面的代码中,带有“-”号的行表示被删除的行,带有“+”号的行表示新添加的行。上面的代码表示用后面5个带有“+”号的行替换前面那个带有“-”号的行。
Clock类写完了,提交代码。现在在IDEA中,已经没有编译失败的错误了。
接下来根据那个类图,从左到右一个一个地编写剩下3个类的代码。首先是UtcTime类。
创建UtcTime类的代码如下所示(CM: Created class UtcTime.):

public class UtcTime extends TimeSubject {
   @Override
   public void notifyAllClocks() {
   }
}

再来实现UtcTime类的notifyAllClocks()方法。
UtcTime类的notifyAllClocks()方法如下所示(CM: Implemented method UtcTime.notify-AllClocks().):

public void notifyAllClocks() {
-
+        for (Clock clock : super.clocks.values()) {
+            clock.setLocalTime(Clock.toLocalTime(this.utcZeroTime));
+        }
     }

呃,类图中utcTime这个名字起得真的让人有点纠结。它有两个含义,既可以指UtcTime这个类的一个对象,也可以指UtcTime这个类中用来保存UTC时间的那个成员变量。为了区分,把后者改名叫utcZeroTime,表示与UTC时差为0的时间。所以在细化后的类图中,除了UtcTime类的类名和PhoneClock类的utcTime成员变量的变量名之外,其他8处出现UtcTime的地方都要改为utcZeroTime。另外发现这个类图还有一个错误,上面那个for循环里面的方法clock.setLocalTime()的参数,不应该仅仅从utcTime改为utcZeroTime,还应该把它转换为时钟所表示的当地时间,因为这是clock.setLocalTime()方法的接口所要求的。可以用Clock类的一个静态方法toLocalTime()来把utcZeroTime转换为local time。
刚根据那个类图写了3个类,就发现了那个图有3个问题需要修改:一个是notify()方法名改为notifyAllClocks(),一个是把8处utcTime改为utcZeroTime,还有一个是for循环里面的那个方法的参数需要转换为local time。我现在就把那个类图打印一份。您一边写代码,我一边用红笔在类图上改。
目前在细化后的类图中对上述3个问题做出的修改如图2-2所示。

在IDEA中,UtcTime类中的notifyAllClocks()方法里的for循环里的那句话,有两处标出了红色,是因为这里有两个编译错误,一个是Clock.toLocalTime()这个静态方法没有定义,另一个是this.utcZeroTime这个成员变量没有定义。
UtcTime类的2处编译错误在图2-3中用箭头标了出来。

“换我来编会儿吧。咱们先解决后一个问题。把光标移动到utcZeroTime上,还是用Alt+Enter快捷键来帮咱们创建utcZeroTime这个成员变量。这个快捷键真是太好使了!”
在UtcTime类里面创建出的utcZeroTime成员变量的代码如下所示(CM: Add
ed an int field utcZeroTime to class UtcTime.):

public class UtcTime extends TimeSubject {
+    private int utcZeroTime;

“接下来处理前一个问题,在类Clock中添加静态方法toLocalTime()。”
在类Clock中添加静态方法toLocalTime()的代码如下所示(CM: Added static method Clock.toLocalTime().):

public abstract class Clock {
-    private final int UTC_OFFSET = 0;
+    private static final int UTC_OFFSET = 0;
     private int localTime = 0; 

     public abstract void setLocalTime(int localTime);
+
+    public static int toLocalTime(int utcZeroTime) {
+        return utcZeroTime + UTC_OFFSET;
+    }
 }

“为了让静态方法toLocalTime()能够访问到成员变量UTC_OFFSET,把这个成员变量也转变为静态的了。现在IDEA里面没有编译错误了。接下来按照细化后的类图,来实现UtcTime类的成员变量utcZeroTime的getter和setter。”
在IDEA里面,可以先把光标定位到UtcTime类的成员变量utcZeroTime下面,然后按快捷键Alt+Insert调出Generate快捷菜单,来让IDEA帮助生成utcZeroTime的getter和setter,如图2-4所示。

“不错,还是快捷键方便。”
生成的UtcTime类的成员变量utcZeroTime的getter和setter的代码如下所示(CM: Generated getter and setter of the field utcZeroTime of class UtcTime.):

public class UtcTime extends TimeSubject {
     private int utcZeroTime; 

+    public int getUtcZeroTime() {
+        return utcZeroTime;
+    }
+
+    public void setUtcZeroTime(int utcZeroTime) {
+        this.utcZeroTime = utcZeroTime;
+    }

根据细化后的类图中的注解框里的伪代码,UtcTime类中的setUtcZeroTime()方法里面应该有个notifyAllClocks()方法,现在就可以加上它。
在UtcTime类中的setUtcZeroTime()方法里添加notifyAllClocks()方法的代码如下所示(CM: Added method call notifyAllClocks() in method UtcTime.setUtcZeroTime().):

public void setUtcZeroTime(int utcZeroTime) {
         this.utcZeroTime = utcZeroTime;
+        notifyAllClocks();
     }

接下来,就可以根据类图编写PhoneClock类了。
创建类PhoneClock的代码如下所示(CM: Created class PhoneClock.):

+public class PhoneClock {
+}

然后根据类图中注解框中的伪代码来实现PhoneClock类中的setLocalTime()方法。
PhoneClock类中的setLocalTime()方法的代码如下所示(CM: Implemented method Phone-Clock.setLocalTime() according to the class diagram.):

-public class PhoneClock {
+public class PhoneClock extends Clock {
+    @Override
+    public void setLocalTime(int localTime) {
+        this.localTime = localTime;
+        this.utcTime.setUtcZeroTime(localTime - UTC_OFFSET);
+    }
 }

“哦,按照类图中注解框中的伪代码写完后,在IDEA的PhoneClock类里面,有3个地方出现了红色的编译错误。”
PhoneClock类的3处编译错误在图2-5中用箭头标了出来。

咱们一个一个看这3个编译错误。第1个编译错误是this.localTime,这个localTime实际上应该来自其父类Clock,所以应该是super.localTime,这是细化后的类图中的错误。相应地,为了让子类能够访问到父类的成员变量,父类Clock中的成员变量localTime也应从private改为protected。需要改一改类图,把这个问题编为4号。
在细化后的类图中对上述问题做出了对4号问题的修改,如图2-6所示。

第2个编译错误是this.utcTime。这是由于在类图中PhoneClock类左侧菱形符号所表示的它所持有的成员变量utcTime还未创建,一会再创建。第3个编译错误是UTC_OFFSET,这个错误的原因与第1个编译错误类似,即UTC_OFFSET实际上也应来自父类,所以父类的成员变量UTC_OFFSET应该改为protected,以便于子类访问,而不应该是private。类图应该再改一下,在图中把这个问题编为5号。
在细化后的类图中对5号问题做出的修改如图2-7所示。

类图改好后,相应地来改代码。
将PhoneClock类中的setLocalTime()方法中的this.localTime改为super.LocalTime的代码如下所示(CM: Made field Clock.localTime protected.):

public class PhoneClock extends Clock {
     @Override
     public void setLocalTime(int localTime) {
-        this.localTime = localTime;
+        super.localTime = localTime;

将父类Clock中的成员变量localTime从private改为protected的代码如下所示(CM同上):

public abstract class Clock {
     private static final int UTC_OFFSET = 0;
-    private int localTime = 0;
+    protected int localTime = 0;

将父类Clock的成员变量UTC_OFFSET从private改为protected的代码如下所示(CM: Made field Clock.UTC_OFFSET protected.):

public abstract class Clock {
-    private static final int UTC_OFFSET = 0;
+    protected static final int UTC_OFFSET = 0;

“好了,现在该修复前面说的第2个编译错误this.utcTime了。还是把光标定位到PhoneClock类中红色的utcTime上,用Alt+Enter快捷键来帮咱们创建这个成员变量。”
在PhoneClock类中创建utcTime成员变量的代码如下所示(CM: Added field PhoneClock.utcTime.):

public class PhoneClock extends Clock {
+    private UtcTime utcTime;
+
     @Override
     public void setLocalTime(int localTime) {

现在IDEA里面没有编译失败的代码了。根据类图现在只剩下最后一个类CityClock了。先用IDEA创建一个新类CityClock。
创建新类CityClock的代码如下所示(CM: Created class CityClock.):

+public class CityClock {
+}

根据CityClock的类图和其注解框中的伪代码,该实现它所继承的setLocalTime()方法了。
CityClock类的setLocalTime()方法的代码如下所示(CM: Implemented method CityClock.setLocalTime().):

-public class CityClock {
+public class CityClock extends Clock {
+    @Override
+    public void setLocalTime(int localTime) {
+        super.localTime = localTime;
+    }
 }

这里又发现一个类图中的错误。类图中CityClock类的注解框中的第2行伪代码的this.localTime应该是super.localTime,因为这个localTime是从父类继承下来的。需要再改一下类图,把这标记为6号问题。
在细化后的类图中对6号问题做出的修改如图2-8所示。
在编写main()方法之前,先回顾一下本章的内容。
1)按照细化后的类图开始编写代码。
2)使用分布式代码版本管理工具git把代码暂时提交到本地代码库,在编译未通过的情况下,一步一步多次地提交代码。

3)每次提交代码都写明Commit Message提交注解。
4)随着编程的进行,修改了细化后的类图中的多处错误。
5)通过操练我们学到了以下技能:
a)在IDEA中把光标定位到那些红色的有编译错误的代码上,然后按快捷键Alt+Enter,能快速帮助我们生成所需要的代码。
b)如果能做到当有少量代码改动时就频繁地把代码提交到本地代码库而不管其是否通过编译,且每次提交都能填写有关此次代码改动的、意图明确的Commit Message,那么这种每次少量且意图描述清晰的代码提交,一方面增强了将来阅读代码变动的可读性,另一方面当代码写错需要回退时也能有助于做到更精细的回退。

时间: 2024-09-26 10:47:45

《驯服烂代码:在编程操练中悟道》一第2章 按图索骥地编写代码的相关文章

《驯服烂代码:在编程操练中悟道》一导读

前 言 程序员好比运动员,要想在竞技场上获胜,需要在训练场里长期刻苦地练习技艺. 程序员好比士兵,要想在短兵相接的白刃战中取胜,需要在练兵场上长期刻苦地练习格斗. 程序员好比调酒师,要想用炫目的技艺为客人花式调酒,需要在业余时间长期练习抛瓶. 运动员与和平年代的士兵有大量的时间用于训练.但绝大多数程序员所在的软件公司,在一个接着一个的项目进度的压力下,无法提供大量的时间来供程序员练习,而很多程序员又不愿意在下班后再碰代码.如此一来,程序员们就成了一直在竞技场上比赛的运动员,一直在敌人面前搏斗的士

《驯服烂代码:在编程操练中悟道》一第1章 刻舟求剑的文档

第1章 刻舟求剑的文档 "什么是软件?"20世纪90年代初的一个冬日,在北京东南部的一所大学里,一位年近花甲的老师,给我们这些计算机系的学生讲软件工程这门课时,问了这个问题.对于当时几乎没有机会接触计算机的我来说,软件就是学校计算机房里那些DEC小型机上令人费解的命令,和286个人计算机里那些好玩的"吃豆子"和"赛车"游戏."软件不仅仅是程序,还包括描述程序的文档.软件就是程序加文档."老师对软件的定义,深深地刻在我的脑子里,

c#代码-C#编程过程中遇到的难题

问题描述 C#编程过程中遇到的难题 我的界面form_load一打开适合一张数据库里面的退货表联系上了的,但是如果我想要查询 订单表的话,怎么做呢?而且是不换界面的,因为我的界面使用的是数据网格来显示表格,当我点击查询的订单表的时候,总是会弹出ConnectionString尚未初始化这个问题?怎么解决呢?以下是我查询的代码,求指导 SqlCommand sqlCommand = new SqlCommand(); SqlConnection myConnection = new SqlConn

android 邮件-android通过代码获取当前系统中安装的所有邮件应用列表

问题描述 android通过代码获取当前系统中安装的所有邮件应用列表 想通过代码得到当前系统所有可以收发邮件的应用,通过列表显示出来,求大神指导,有知道的朋友请知道下获取的方法,目前我能想到的就是获取当前所有安装的应用通过关键字过滤,不过感觉这种方法不是很好,不能保证所有过滤出来的都是邮件应用.

代码混淆-android混淆代码后崩溃日志中不显示行号的问题

问题描述 android混淆代码后崩溃日志中不显示行号的问题 android混淆代码后崩溃日志中不显示行号,找崩溃的地方很不方便,如何解决,求大神指点,谢谢! 解决方案 问题已解决.原因是在混淆代码时默认会去掉class文件中的调试信息(源码的行号.源文件信息等),需要在混淆配置文件中申明保持这些信息: -renamesourcefileattribute SourceFile -keepattributes SourceFile,LineNumberTable 解决方案二: tks, 这个问题

xml解析-java代码怎么读取xml中的各个属性值对应的内容

问题描述 java代码怎么读取xml中的各个属性值对应的内容 java代码怎么读取xml中的各个属性值对应的内容.比如得到连接Sql Server数据库的localhost sqlname username password

点击btn动态创建checkbox怎么编写代码?新手求教育

问题描述 点击btn动态创建checkbox怎么编写代码?新手求教育 点击btn动态创建checkbox怎么编写代码?新手求教育 inline code protected void btn2_Click(object sender, EventArgs e) { } 解决方案 页面上添加checkbox,设为隐藏,点击btn后将它显示不可以吗?

Java 编程技术中汉字问题的分析及解决(转)

编程|汉字|解决|问题 Java 编程技术中汉字问题的分析及解决 段明辉自由撰稿人2000 年 11月 8日内容: 汉字编码的常识 Java 中文问题的初步认识 Java 中文问题的表层分析及处理 Java 中文问题的根源分析及解决 Java Servlet 中文问题的根源 修改 Servlet.jar 中文乱码的处理函数 参考资料 作者简介在基于 Java 语言的编程中,我们经常碰到汉字的处理及显示的问题.一大堆看不懂的乱码肯定不是我们愿意看到的显示效果,怎样才能够让那些汉字正确显示呢?Jav

Java 编程技术中汉字问题的分析及解决,文件操作

编程|汉字|解决|问题 在基于 Java 语言的编程中,我们经常碰到汉字的处理及显示的问题.一大堆看不懂的 乱码肯定不是我们愿意看到的显示效果,怎样才能够让那些汉字正确显示呢?Java 语言 默认的编码方式是UNICODE ,而我们中国人通常使用的文件和数据库都是基于 GB2312 或者 BIG5 等方式编码的,怎样才能够恰当地选择汉字编码方式并正确地处理汉字的编 码呢?本文将从汉字编码的常识入手,结合 Java 编程实例,分析以上两个问题并提出 解决它们的方案. 现在 Java 编程语言已经广