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

简介:

本节书摘来自华章出版社《驯服烂代码:在编程操练中悟道》一书中的第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类的快捷菜单。

image

“哦,这么方便!您要是不说,我还要傻乎乎地一点点地写呢。”
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所示。

image

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

image

“换我来编会儿吧。咱们先解决后一个问题。把光标移动到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所示。

image

“不错,还是快捷键方便。”
生成的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中用箭头标了出来。

image

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

image

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

image

类图改好后,相应地来改代码。
将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把代码暂时提交到本地代码库,在编译未通过的情况下,一步一步多次地提交代码。

image

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

相关文章
|
存储 关系型数据库 MySQL
【MySQL专题】MySQL百万级数据插入效率优化
【MySQL专题】MySQL百万级数据插入效率优化
1288 0
【MySQL专题】MySQL百万级数据插入效率优化
|
Cloud Native Go API
Go语言在微服务架构中的创新应用与实践
本文深入探讨了Go语言在构建高效、可扩展的微服务架构中的应用。Go语言以其轻量级协程(goroutine)和强大的并发处理能力,成为微服务开发的首选语言之一。通过实际案例分析,本文展示了如何利用Go语言的特性优化微服务的设计与实现,提高系统的响应速度和稳定性。文章还讨论了Go语言在微服务生态中的角色,以及面临的挑战和未来发展趋势。
|
存储 编解码 数据库
【C++ 包装器类 std::tuple】全面入门指南:深入理解并掌握C++ 元组 std::tuple 的实用技巧与应用(二)
【C++ 包装器类 std::tuple】全面入门指南:深入理解并掌握C++ 元组 std::tuple 的实用技巧与应用
560 0
|
机器学习/深度学习 人工智能 算法
清华接手,YOLOv10问世:性能大幅提升,登上GitHub热榜
【6月更文挑战第6天】清华大学团队推出YOLOv10,实现目标检测性能大幅提升。该算法在效率和准确性间取得更好平衡,解决NMS后处理问题,优化模型架构,减少参数和FLOPs。YOLOv10在COCO基准测试中表现出色,虽有未在大规模数据集预训练及小规模模型性能差距的局限,但已成实时检测领域重要进展,引领未来研究方向。[链接](https://arxiv.org/pdf/2405.14458)
506 1
|
人工智能 机器人
【超简单】让AI自动回复抖音评论
大家在刷抖音的时候,是不是发现有些视频底下的作者评论像是AI回复的,不是真人回答的。今天就来教大家,怎么用AI自动回复你在抖音发布视频中的评论。
3682 0
|
机器学习/深度学习 人工智能 自然语言处理
阿里云人工智能的考试内容是什么?考试有几个等级?
人工智能是一个新产业,而且正逐渐占据我们的生活,帮助我们解放生产力、完成很多危险的工作,现在越来越多的企业会使用人工智能、研发人工智能,以求可以占据更多的市场份额。
846 0
|
资源调度 运维 Java
定时任务报警通知解决方案详解
随着微服务和云计算的兴起,定时任务技术也是发展迅速,不仅能做单机的定时任务,而且在分布式系统下应用也很广泛,成为了业务做兜底、数据处理的第一选择。
2776 3
定时任务报警通知解决方案详解
|
监控 安全 数据安全/隐私保护
grafana 目录遍历 (CVE-2021-43798)
grafana 目录遍历 (CVE-2021-43798)
689 0
grafana 目录遍历 (CVE-2021-43798)
|
前端开发 JavaScript 数据库
【前端】JavaScript 实现下载图片但不自动预览图片
【前端】JavaScript 实现下载图片但不自动预览图片
1242 0
|
机器学习/深度学习 消息中间件 存储
监控指标10K+!携程实时智能检测平台实践
本文将介绍携程实时智能异常检测平台——Prophet。到目前为止,Prophet 基本覆盖了携程所有业务线,监控指标的数量达到 10K+,覆盖了携程所有订单、支付等重要的业务指标。Prophet 将时间序列的数据作为数据输入,以监控平台作为接入对象,以智能告警实现异常的告警功能,并基于 Flink 实时计算引擎来实现异常的实时预警,提供一站式异常检测解决方案。
监控指标10K+!携程实时智能检测平台实践

热门文章

最新文章