1.1 如何学习新的编程语言
一名软件工程师的最大挑战就是使自己的技术栈跟得上技术的发展,而在这个技术飞速发展的时代,保证自己不被淘汰的唯一方法就是不断学习。
那么,程序员需要掌握多门编程语言吗?很多初学者都被这个问题所困扰。Google研究总监 Peter Norvig曾就这个问题给出自己的观点:一名优秀的程序员至少应该掌握 6门编程语言,其中包括支持类抽象的编程语言如 Java 或 C++,支持函数抽象的编程语言如 Lisp或 ML,支持语义抽象的编程语言如 Lisp,支持声明规范的编程语言如 Prolog 或C++模板,支持协程的编程语言如Icon或Scheme,以及支持并发的编程语言如Sisal。
一名画家若擅长使用多种类型的画笔,就可以创作出多种类型的艺术画作;一名程序员若掌握多种类型的编程语言,在解决问题时就可以有多种选择。
1.1.1 重点学什么
《计算机程序的构造和解释》的作者曾经表达这样的观点:在学习 一 门 新 的 编 程 语 言 时 , 应 该 关 注 这 门 语 言 的 基 本 表 达 形 式(Primitive Elements)、组合的方法(Means of Combination)及抽象的方法(Means of Abstraction)这三个特性。
如果展开以上三个特性的话,就几乎包含了学习一门编程语言所需要关注的所有重要知识。
◎ 基础知识:基本语法、关键字、变量与常量、数据类型、运算符、流程控制、异常处理、文件处理、编程思想(面向对象、面向过程、函数式编程)、多线程支持等。
◎ 应用知识:网络请求、数据处理、内置函数、对日志和调试的支持、对单元测试的支持、序列化与反序列化等。
◎ 高级知识:开源类库、开源框架、底层原理等。
1.1.2 学习方法
学习编程需要长期坚持,不要迷信三五天就能让人学会的教程,你可能三五天掌握了一些语法,却难以完成复杂一点的编程,也没有和同行交流的经验。
1.选择适合自己的编程语言
如果想学习一门新的编程语言,又不知道学习哪一门的话,可以参考 TIOBE 编程语言排行榜,其榜单每个月都会更新,可以反映某编程语言的热门程度。
在学习编程语言之前,需要先简单了解其主要特性及可以解决的问题,即选择适合自己的编程语言,带着目的去学习。
如表1.1所示为一张关于编程语言分类的表格,读者在选择编程语言进行学习时,可以参考这张表,选择最适合自己的编程语言。比如,你擅长Java,希望学习另一门编程语言来提升自己的竞争力,就可以先选择和 Java相似的编程语言,例如和 Java同为强类型的、解释型的编程语言,再根据TIOBE编程语言排行榜就知道Python值得学习。
表1.1
当然,选择编程语言的标准不尽相同,比如想从事手机App开发,就需要学习和移动端开发有关的编程语言,比如 Object-C、Java 和Swift;如果对区块链技术感兴趣,想要从事区块链开发,那么Go、Python、Solidity、C++等会是不错的选择。
2.选择好的学习方式
在学习新的编程语言时,笔者认为读书、看视频和参加培训都是不错的学习方式。当然,不同的学习方式适合不同的人。
很多人通过阅读书籍进行学习,因为他们觉得书籍上的内容相对完善且成体系,并认为通过视频和课程学习会比较慢。关于编程的
书,大概有入门类、工具类、实战类、进阶类、原理类等,可以根据自己的知识程度进行选择,切勿盲目选择。
还有些人认为在阅读书籍的过程中会遇到很多没见过的名词、定义等,容易阻碍学习的进度。他们愿意选择偏重实践的内容,倾向于课程、视频等方式,因为可以进行现场敲代码、排查问题等。
各种学习方式并无好坏之分,适合自己才最重要。如果能够将多种方式相结合,通过书籍完善自己的知识体系,并提升理论知识,再通过视频及课程增加自己的实战经验,就再好不过了。另外,在学习新语言时,翻阅官方文档和源码也是必不可少的,当然,这比较适合在学习的中后期进行。切勿遗漏这个步骤,这是了解并掌握一门语言的至关重要的步骤。
3.勤加练习
很多开发者容易陷入误区,只注重理论知识的学习,不注重实战,在回答别人问题的时候头头是道,一旦动手实践却不知所措。所以,学习一门编程语言,是绝对离不开动手实践的。我们要把从书本中学习到的理论知识和实际应用结合起来,由浅入深地学习,最终达到熟能生巧的目的。孔子说“学而不思则罔,思而不学则殆”,在学习编程语言的过程中,学和思固然重要,勤加练习却也是必不可少的。
在学习编程语言的过程中进行练习,可以增加自己对理论知识的理解,增强自己的记忆。我们都知道,Java中的 int是有范围的,书本上说如果超过范围就会溢出,那么这个范围到底是多少,溢出之后的表现是什么呢?只有真正地敲一遍代码,真正地练习一下才会有深刻的体会,才能在日后的工作中避免发生类似的错误。
另外,在练习的过程中难免会遇到各种各样的问题。比如,Java初学者在安装 JDK和配置环境变量时可能会遇到很多问题,其想办法解决问题的过程非常可贵,因为在日后的工作中能够自主解决各类问题,是一名优秀程序员的必备技能。在很多时候,初级程序员和高级程序员之间最突出的区别其实就是解决问题的能力。通过实践,我们也可以锻炼自己在这方面的能力。所以,在实践的过程中遇到任何问题都不要退缩和逃避,要勇敢地面对并解决问题。
4.带着问题学习
学习要由目标驱动,在目标驱动起作用后,我们还可以采用问题驱动方式进行学习,即在学习过程中多问问题。
问问题可以采用六何法。六何法,又叫作6W或5W1H分析法,即What、Who、When、Where、Why及How,需要我们在学习的过程中多思考、多问问题。举个简单的例子,在学习设计模式中的单例模式时,可以用六何法多提几个问题,例如:
◎ 什么是单例模式?
◎ 什么时候使用单例模式?
◎ 怎么实现单例模式?
◎ 哪种单例实现方式最好?
◎ 在单例模式中如何保证线程安全?
在学习的过程中,如果没被问题驱动,你学到的就可能只是一个技术概念和用法。有了六合法的问题驱动,你学到的就会从一个点横向扩展成一条线,如果将线上的每个点都逐渐深入,就会扩展成一个面。比如,我们用六合法来学习单例时,就可能以单例模式为中心点扩展到线程安全、锁、序列化、枚举、类加载机制等知识。
5.教是最好的学
通过写博客来学习也是非常棒的一种学习方式,这对于新技术的学习十分有效,还可以通过技术分享、线下会议及线上教学等方式将自己学到的知识分享给他人,这就是教学学习法。
教学学习法有如下好处。
◎ 迫使自己更深入地了解更多的知识。
◎ 在教学的过程中会加入自己的理解。
◎ 可以回头翻看教学的内容。◎ 可以加深记忆。
◎ 可以和别人深入探讨。
1.2 代码规范与单元测试
2017年,阿里巴巴发布编码规范,这是开源界的一件大事,也在知乎等平台上引发了广泛的讨论,其中有个别回复纠结于具体细节的商榷和建议,但大部分认同该规范的指导意义。本节拟从代码规范、单元测试、代码审查及审查清单谈谈笔者的一些体会。
1.2.1 编码规范
不以规矩,不能成方圆。为什么要有规范(规约)想必不用多说了。本书重在讲述程序员如何具备大局观,具备大局观所要涵盖的知识宽度和视野,因此对于具体编码规范的逐条解析不作为重点。
Google Java Style Guide包含的内容有源文件基础、源文件结构、格式、命名、编程实践和 Javadoc,可以作为一个团队必须遵守的共识。一套好的规范应该搭配好的审查清单(CheckList)。《Java开发手册 1.5.0》就是一个不错的融合规约和审查清单的案例,其中的单元测试一章有一条规范检查项,引用如下:
【强制】单元测试应该是全自动执行的,并且是非交互式的。测试框架通常是定期执行的,执行过程必须完全自动化才有意义。对输出结果需要人工检查的测试不是好的单元测试。在单元测试中不准使用System.out进行人肉验证,必须使用Assert进行验证。
该项可以作为一条独立的审查清单纳入CheckStyle或者PMD这样的工具来扫描静态代码。对于单元测试应该如何编写,会在下面的单元测试小节展开讨论。
1.2.2 单元测试
单元测试(Unit Testing,UT)说起来简单,在实际操作过程中却要注意它不是为了测试而测试的。如下所示是一段对Java中的字符串进行右对齐操作的代码:
测试代码如下:
如何采用测试驱动设计(Test-Driven Development,TDD)方式写这段业务代码呢?这里先看一下要满足的需求:按照指定的目标长度扩展并右对齐字符串,用指定的字符串填充目标字符串的左边。所以,这个需求对应的测试用例如下。
◎ 如果源字符串为null,则无论指定目标长度为多少,结果都为null。
◎ 如果源字符串为""(空串),指定目标长度为3、填充字符串为"w",则结果为"www"。
◎ 如果源字符串为"bat",指定目标长度为3、填充字符串为"yw",则结果为"bat"。
◎ 如果源字符串为"bat",指定目标长度为5、填充字符串为"yw",则结果为"ywbat"。
◎ 如果源字符串为"bat",指定目标长度为1、填充字符串为"yw",则结果为"bat"。
◎ 如果源字符串为"bat",指定目标长度为-1、填充字符串为"yw",则结果为"bat"。
1.2.3 测试驱动设计
笔者在 2010 年做了一件现在看起来不靠谱的事情,就是把一个模块的代码测试覆盖率做到了80%,在笔者刚接手时其覆盖率是30%。为此,笔者让一位研发人员补测试补了一周,这可以算作为了覆盖率而做,但其效果如何,我们不敢抱太大希望,比如在下一次重构的时候,这些测试代码是否值得信赖。
有位咨询师提到,测试覆盖率低的病灶常常如下。
◎ 团队成员没有写测试的习惯,没有意识到写测试的重要性,不想写。
◎ 代码难于测试,不会写。
◎ 赶进度,没有时间写。
相对于提升测试的覆盖率,解决这些问题要复杂、棘手得多。笔者不确定上述问题在特定环境下的解决方法是什么,但很确定补测试不是良方,它往往会催生出没有 Assert的畸形测试及大量针对getter、setter的无用测试。
这件事情给笔者一个教训,就是单元测试一定不能事后补。我们团队在项目实际操作过程中没有严格遵循测试驱动设计的步骤,但是遵循了同步编写开发代码和测试代码的思路。
测试驱动设计的基本思想就是在开发功能代码之前先编写测试代码,也就是说在明确要开发的需求之后,首先思考如何分析这个需求,并完成测试代码的编写,然后编写相关代码来满足这些测试用例,最后循环添加其他测试用例,直到该需求对应的测试用例都测试通过。
测试驱动设计有3个原则,如下所述。
◎ 原则1:无测试,不代码。
◎ 原则2:单元测试不在多,能够识别出问题即可。
◎ 原则3:代码不在多,让当前单元测试全部通过即可。
下面看看具体的操作步骤,以1.2.2节的需求为例。
第1步:金丝雀测试
“金丝雀测试”的概念来自早期的煤炭矿井行业:金丝雀对有毒气体比较敏感,在 19世纪左右,英国的矿井工人在下矿井时常常会带一只金丝雀,如果矿井内的有毒气体超标,金丝雀就会立刻死亡,这会救矿井工人一命。
同样,在测试驱动设计实践中,在开发具体的测试用例之前,也需要先写一个dummy的测试用例,确保整个编译、运行和JUnit环境是正常运行的。
之后运行这个测试用例,结果符合预期,说明整个编译、执行和JUnit环境是好的。
第2步:编写第1个最简单的单元测试
对应的需求为:如果源字符串为null,则无论指定的目标长度为多少,结果都为null。编写测试代码如下:
好,在第1个单元测试运行时出现红色,说明编译出现了问题。开始编写如下代码:
这样,第1个单元测试在运行时就是绿色的了。也许有人会问:“这段代码有用吗?”然而,从另一方面来说,这不就是实现当前需求的最简洁和最高效的实现吗?
第3步:编写第2个单元测试
对应的需求为:如果源字符串为""(空串),指定目标长度为2、填充字符串为"w",则结果为"ww"。编写单元测试,代码如下:
测试时再次出现红色,将代码修改如下:
这段代码非常具体(Specific),没有通用性(Generic),但是不妨碍非常高效地满足了当前的需求。
第4步:继续加需求
对应的需求为:如果源字符串为""(空串),指定目标长度为3、填充字符串为"w",则结果为"ww"。
这个需求其实是和上一个需求等价的,所以可以把这个单元测试和上一个单元测试合并:
再运行一次,结果怎样呢?运行结果正确!
第5步:接着加需求
对应的需求为:如果源字符串为"abc",指定目标长度为 3、填充字符串为"w",则结果为"abc"。
增加测试用例,代码如下:
运行一次单元测试,发现运行失败。看来我们要继续写代码了,再加入一个if块:
第6步:再次加需求
对应的需求为:如果源字符串为"abc",指定目标长度为5、填充字符串为"wxy",则结果为"wxabc"。相关测试代码如下:
实现代码如下:
第7步:重构,是为了更好地前行
到目前为止,我们的代码已经“生长”到 30 行了,现在选择重构当前代码,主要关注两方面:让代码更整洁;让应用“从特殊到一般”来泛化代码,使“算法”更清晰。
首先,为了提取“pattern”,我们发现第1个if块和第3个if块有些类似。为了让它们呈现一样的“pattern”,我们在第1个if块中加入一条dummy语句:
然后,为了发掘pattern,对下面的代码块进行重构:
这段代码的逻辑是,当输入的源字符串长度为零,而填充字符的长度为1时,重复利用填充字符进行填充。我们可以重构“重复利用填充字符进行填充”这段代码:
这样的话,就可以把这个if块重构为和其他if块相似的pattern:
这样一来,就可以通过合并第1个if块和第3个if块进行重构:
重构的结果如下:
第8步:继续加入需求
对应的需求为:如果源字符串为"abc",指定目标长度为 1、填充字符串为"wxy",则结果为"abc"。相关测试代码如下:
运行单元测试,结果居然是通过!
第9步:继续加入需求
对应的需求为:如果源字符串为"abc",指定目标长度为-1、填充字符串为"wxy",则结果为"abc",也就是说不进行处理。相关测试代码如下:
运行单元测试,结果仍然是通过。
也就是说,在前面的重构中,在使用了“从特殊到一般”对代码进行泛化重构之后,代码的使用范围更广了,或者说隐藏在背后的算法显现了,这也说明测试驱动设计是可以演化出算法的。
版权归原作者 程序员小英 所有, 如有侵权,请联系我们删除。