逸言

推行TDD的思考

| Comments

目前来看,推行TDD的障碍大约有如下几点:

1. 开发人员的质量意识;
2. 分析需求并进行任务分解的能力;
3. 将测试作为开发起点的开发习惯;
4. 开发人员的重构能力,包括如何识别坏味道和如何运用重构手法;
5. 单元测试的基础设施,尤其是测试数据准备;

开发人员的质量意识

开发人员对于软件质量,常常偏重于软件的外部质量,体现在他们的工作效益上,就是被测试人员发现的缺陷数。而惯常的软件开发思想,总是认为开发人员不适合做测试,因为他们总是站在自己的角度去看待问题,从而可能忽略真正需要测试的用例。这种思想给了开发人员一个错误信号,认为自己不应该写测试,即使写了测试,也写不好。殊不知,由开发人员编写测试带来的收益,最重要的一点不在于测试本身,而在于它能促进开发、测试以及需求分析人员的交流与沟通。而测试先行的方式也能让开发者跳出实现的窠臼,而从业务角度去看待问题,从消费者角度去思量接口。此外,由于开发者总是惫懒地将测试职责委派给了专门的测试人员,于是渐渐会产生一种依赖心理。测试人员的精确测试当然可以保障质量,但这种测试通常是黑盒测试,这里保障的质量主要还是外部质量。而且,这种测试带来的反馈总是慢于开发进度,一旦发现缺陷,修复缺陷的成本也会变得更高。

软件质量除了外部质量之外,内部质量同等重要。软件成本等于开发成本与维护成本之和,而维护成本的增加主要就归咎于内部质量的糟糕。这里讲的内部质量包括:代码的可读性、可重用性、可扩展性等。当我们让开发人员为原有代码编写单元测试时,总是觉得举步维艰。分析原因,主要问题在于代码的可测试性不好。要测试一个类,竟然连简单创建它的对象都变成了不可能完成的任务。我们为这样的代码编写单元测试,就好像在触及蜘蛛网,一旦被这些网丝给牵住,缠住,就可能无法摆脱。除非我们能够快刀斩乱麻,那对于这个系统而言,就不是维护,而是重写了。测试先行的开发至少在一定程度规避了这样的问题。即使代码的内部质量仍有所欠缺,但在足够覆盖率的保护下,我们要进行重构也变得更为简单。

Git的使用场景

| Comments

无论学习什么技术,都需要了解该技术的本质。若是靠死记硬背该技术提供的方法或者语法,终归是知其然而不知其所以然,当发现错误时,你根本不知道是什么原因导致的。我在使用Git时,就处于这种知其然而不知其所以然的状态。现在,再来补补课。

Git有三个工作区域,分别为:工作目录(Working Directory)、暂存区(Stage或Index)以及资源库(Repository或Git Directory)。下图是文件在这三个工作区域之间的关系:

参考Pro Git一书,它给出了Git的几个要点: * 直接快照,而非比较差异:Git与其他版本管理系统的主要差别在于,Git只关心文件数据的整体是否发生了变化,而其他多数版本管理系统则只关心文件内容的具体差异。Git并不保存文件前后变化的差异数据,更像是把变化的文件做一个快照,然后记录在一个微型的文件系统中。每次提交更新时,会比较这个快照。若文件没有变化,Git则只对上次保存的快照作一个链接。你可以理解Git就是一个小型的文件系统。 * 近乎所有操作都可本地执行:无需多说,这本身就是分布式版本管理系统的特征。 * 时刻保持数据完整性:保存到Git前,所有数据都要进行内容的校验和(checksum),并将该结果作为数据的唯一标识。Git使用了SHA-1算法计算数据的校验和,并将该结果作为索引,而非文件名。 * 多数操作仅添加数据

玩花招的PowerMock

| Comments

当我们面对一个遗留系统时,常见的问题是没有测试。正如Michael Feathers在Working Effectively with Legacy Code一书中对“遗留代码”的定义。他将其简单归纳为“没有测试的代码”。真是太贴切了!正是因为没有测试,使得我们对遗留代码的任何重构都有些战战兢兢,甚至成为开发人员抵制重构的借口。从收益与成本的比例来看,对于这样的系统,我一贯认为不要盲目进行重构。因为重构的真正适用场景其实是发生在开发期间,而非维护期间。当然,提升自己的重构能力,尤其学会运用IDE提供的自动重构工具,可以在一定程度上保障重构的质量。然而,安全的做法,还是需要为其编写测试。

测试是分层的,即使是针对自动化测试。面对遗留系统,成本相对较低的是针对功能特性编写的功能测试(或者说是验收测试),这可以运用一些BDD框架如Cucumber、JBehave等。由于它的测试粒度较粗,可以以较少的测试用例覆盖系统的主要功能。然而,它的缺点同样存在,那就是反馈周期相对较长。这就好像你置身一个陌生的城市,在找不到路的情况下,只是跟着感觉走。走了数十公里之后,方才幡然醒悟,想起要翻一翻带在手上的地图。倘若发现方向走错,再要回转就已经晚了。反馈周期最短的自然是单元测试。同样根据Michael Feather的定义,单元测试一定要快,一定要不依赖于外部资源。单元测试的粒度自然是最小的,但不要直观地认为单元测试就是针对方法。若只是针对方法来编写单元测试,就会陷入为测试而测试的怪圈。即使是位于技术象限的单元测试,我们仍然要按照业务规则来编写。一个测试方法应该对应一个粒度最小的原子功能。

使用Maven为一个项目生成多个Jar包

| Comments

虽然在Java平台下,各种构建工具如Maven、Gradle、SBT已经得到了较为广泛地运用,同时Maven约定的模块目录结构也得到了业界的认可,成为了Java平台下项目结构的事实标准。但我们总无法避免与各种遗留系统或老系统打交道。在没有Maven的时代,是Ant统治的时代,它因为提供了较为灵活的编写Task的功能,而忽略了制定一套看似呆板,实则有效的标准模板。于是,在不同的企业,不同的Java项目,出现了各种各样奇怪的目录结构与打包要求。拖着这些沉重的历史包袱,我们自然希望彻底革命,把那些看不顺眼的东西全部改造,让整个项目焕然一新。这是好事儿,所谓长痛不如短痛,一下子把问题肿瘤给割了,痛快!可是现实总有那么一些阻力会让我们缩手缩脚,我们不能挥起革命的利刃一阵乱砍,这弄不好会砍伤自己的手。于是乎,我们需要做战略性的撤退。退一步海阔天空嘛。

我面对的就是这样一个软件系统。这个Java开发的软件系统一直没有依赖管理,仅仅编写了Ant任务用于发布打包。我们的任务是渐进地引入Maven,并在从Build到deploy的整个生命周期中,逐步替换Ant,与持续集成搭配起来。这个系统的多数模块都划分了服务端与客户端。然而不巧的是,各个模块的服务端和客户端都集中在一个模块中。同时,这个项目的目录结构并非标准的Maven结构,如下图所示。因此,还需要自定义Source与TestSource的目录结构。在原来的Ant任务中,是将它们打包成了两个Jar包。现在,我们需要在Maven中同样做到这一点。

使用Guava提供的Preconditions

| Comments

我看到这么一段Java代码,主要逻辑是对传入的数据对象进行分析和处理。方法除了长了一点,本身没有什么问题,但一个不好的味道是方法中充斥了一部分对对象非空的处理,以及写日志的信息。这些语句本身不属于方法的主干实现,却很讨厌地干扰了方法的主要逻辑,影响了程序的可读性。

public class Processor {
  public void execute(ProcessData processData) {
      if (processData == null || processData.getInput() == null) {
          logger.error("processData is error");
          return;
      }

      List<FavoratePolicy> policies = fetchPolicies(processData);
      if (policies == null || policies.isEmpty()) {
          logger.error("can not fetch favorate policies");
          return;
      }

      for (FavoratePolicy policy : policies) {
          //handle policy
      }
  }
}

我最初想要尝试使用Guava提供的Optional。经过仔细分析,发现这里的场景并不适合Optional。Optional确实可以用于处理null object,甚至可以将其看做是Null Object模式的实现。但它为Java语言带来的好处,无非是提高了代码可读性,同时可以有效地防止程序员忘记对null的判断。它不能带来代码量的减少,用在这里,也无法消除非空判断以及日志的噪音。所幸,Guava还提供了Preconditions,它拥有更好而简洁的断言方式,可以处理程序的一些异常分支。它主要定义的方法包括checkArgument(), checkNotNull()以及checkState()等,可以通过抛指定异常的方式,将程序的主分支与异常分支有效地隔离开。通过引入checkArgument()方法,前面的代码可以调整为:

public class Processor {
  public void execute(ProcessData processData) {
      try {
          checkArgument(processData != null && processData.getInput() != null, "processData is error");

          List<FavoratePolicy> policies = fetchPolicies(processData);

          checkArgument(policies != null && (!policies.isEmpty()), "can not fetch favorate policies");
      
          for (FavoratePolicy policy : policies) {
              //handle policy
          }

      } catch (IllegalArgumentException ex) {
          logger.error(ex.getMessage());
      }
  }
}

经过这样的处理,程序的主体逻辑变得清晰了许多,而且也能够有效地避免日志方法的重复。当然,这里也可以使用checkNotNull()方法来处理ProcessData与List的null情形。但由于它抛出的异常为NullPointerException,用在这里会增加一个异常捕获(因为它无法处理isEmpty()的情况),所以就一致地选择了checkArgument()方法。不过,在使用这一方法时,要注意它传入的条件表达式与原有实现的表达式是相反的。

关于《恰如其分的软件架构》

| Comments

华中科技大学出版社的徐定翔问我意见,了解我对Just Enough Software Architecture这本书的观感,看是否值得引进。时间是在2010年。从一开始,我就被书名中的Just Enough理念所吸引。它让我想起宋玉的东家姑娘,“增之一分则太长,减之一分则太短”那种不可言说的美丽。我在心里说,架构设计就需要这样。我当时并没有看到本书,只是到Amazon上找到了几篇英文原版样章。犹记得我在读到第一章介绍的RackSpace案例时那种兴奋之情。于是,我迫切地向徐定翔强烈推荐引进这本书,而我则毛遂自荐,希望能作为本书的译者。之后,我在InfoQ上看到对本书的《访谈和书摘》,进一步加强了我翻译本书的信念。于是,出版社开始与作者Fairbanks联系,然而从此音讯如石沉大海。时针指向2011年,我对于本书是否被引进,是否由我翻译,一切未知。我觉得我可能错过了它,思之仍觉怅然。谁知到了九月,消息突然确定,而徐大编辑就不容分说地直接把原书给我寄来了。

拿到沉甸甸的书,第一面就为本书的装帧而惊喜。心里想,我这一辈子若能写出这样一本书,绝对值得生命走过的这一遭了。我并没有迫不及待地开始翻译,这就好似遇到珍馐美味,需得先赏其色,闻其香,然后再品其味。我每天抱着这本书饶有兴味地开始阅读之旅。阅读之旅确乎如行山阴道,沿途之美,目不暇接;可一想到翻译,这种美景就成了一种折磨,因为我害怕辜负这一美景。翻译之初我就举步维艰,那些词语放在那里,我却无法解开“封印”将它们取出来,即使取出来,却又找不到存放的合适位置。一些翻译隐隐约约浮现着,当我竭力去揭开这些词语的真面目时,无论如何用力,总也不能够着。翻译就好像那些年我们一起追过的女孩——追不到,痛苦;追到了,销魂。翻译进度像蜗牛一样的爬着,我终于决定求助了。辗转寻找了好多朋友,都以各种理由拒绝或者放弃了。翻译讲解软件架构的书,确乎不是一件轻松的事儿。那个时候,我的Buddy肖鹏正从翻译《面向模式的软件架构》第五卷的泥潭中爬了出来。每一提及他的这段翻译经历,脸上就会浮现出不堪回首的表情,如看了恐怖片。终于,事情得到转机,最开始是倪健的雪中送炭,再有高翌翔的锦上添花,随着我们这个三人组的建立,翻译才算开始走向正规,我才有了交稿的信心。

自从开始翻译这本书后,我与人谈架构,动辄就会提及“Just Enough,恰如其分”。我像祥林嫂一般地推介着Fairbanks提出的风险驱动模型,并认真地实践着这一模型。我开始对演进的架构有了更深入的理解。我写了《设计恰如其分的架构》这篇博客来详细阐述我对演进式架构的理解。在2011年我参加的技术会议上,我也反复讲解了如何遵循简单之美的原则,运用风险驱动模型设计恰如其分的架构。2012年,在我参加的一个项目中需要针对遗留系统进行技术栈迁移。我撰写了文章《遗留系统的技术栈迁移》,提到了“风险驱动模型”,并在2013年的Scrum Gathering会议上分享了我的一些想法。当然,这个模型并不是锤子,更不是银弹。它更近似于质量属性驱动的架构设计,我们要满足的质量属性,可能就是我们在做架构时需要面对的风险;而在Roy Fielding的那篇关于REST的著名论文中,也提到了对约束的识别,并演示了如何从一个空约束,通过逐步添加约束演化为REST风格的架构。从某种程度上,架构的约束可能是一种风险,也可能成为设计的驱动力。

前几天,我参加Agile China 2013,与我新认识的一位朋友范钢聊到了关于架构重构的问题。事实上,面向对象软件开发到现在,已有十余年之久;各种经验、模式与原则甚嚣尘上并得到较好的推广。然而新的方法、新的语言乃至新的思想仍然层出不穷,尤其是在互联网开发、大数据处理以及移动开发的冲击下,传统软件开发似乎已经开始走向末路。“只见新人笑,不见旧人哭”!??是,也不是。实际上,在传统的企业开发领域,各种大型系统仍然像一艘庞大如巨型海兽一般的船舰在海面缓缓行驶,它或许就是沉没之前的泰坦尼克,一切还都安然无恙,你甚至可以听到船头甲板传来的悠扬的小提琴声;然而,冰川就在远处出没,船长还未察觉。我们该怎么办?这样的巨型船舰,自然不可能如艨艟快艇那般的敏捷,即使是360度的转身,也可以玩得如此漂亮、优雅。这些大型的企业级软件系统已经走过了漫长的历程,它们如此巨大以至于我们只能看到它的一角,它们的零部件如此复杂以至于没有人能够彻底弄懂。我们必须认识到,这些系统是最有权力的系统,它们很有可能掌握了人类生活的根本命脉——金融管理、股市交易、生命健康、医疗管理、机械制造、国防安全、航空、航天……它们就像政治界、金融界的那些巨头,要是患了病咳嗽两声,也许世界都要抖一抖。它们可以轻易改变吗?不能!然而若是不寻求改变,这些系统会宿命地走向衰亡。若我们无法承受重写的成本,唯一的办法或许就是架构的重构。我们必须清晰地认识到这一点。而我认为,风险驱动模型恰恰可以作为架构重构的指导原则。在进行重构之前,我们需要充分评估重构的价值,回答“为什么我们需要重构”的问题;然后去识别风险。在开始重构之前,我们需要尽可能做到万无一失。风险自然是不可避免的,但如果我们能事先识别出这些风险,就能有的放矢地选择正确的技术。风险驱动模型的第三步,则是评估风险是否得到有效缓解。不要轻视这一步!重构往往意味着还债。可是,我们该用什么来说服管理者们付出成本去做一些看似没有产生直接利益的任务呢?答案就是用数据来说话,通过比较重构前后的系统健康指标,可以加重说服老板的砝码。当然,毫无疑问,这个过程一定是迭代的。

我想,通过这次交谈,我进一步找到了“风险驱动模型”适用的场景。而这正是我翻译并推荐本书的根本意义。本书可以在当当网购买。

ThoughtWorks读书雷达-方法学篇

| Comments

方法学(Methodology)看似与开发技能无关,常为程序员忽略。忽略意味着未曾察觉,却不等于它的无关紧要。我们每时每刻呼吸的空气同样会被我们忽略,但空气不重要么?不要等到我们开始戴上口罩,每日关心PM 2.5指数时,方才察觉原来空气的质量已经积重难返了。在我们的读书雷达中,方法学象限囊括的书籍多与开发过程相关。如果说开发技能是程序员修炼过程中必须加强的显式的力,则开发过程的这些思想与实践,就是隐藏的推手。你若不动,它会推你前行,然而缓慢;若你后退,则推动的阻力就大,暗流汹涌。只有自愿向前,这种助力才能如鱼得水,使你能够优游地前行。

软件开发说通了,不是技术问题,而是人的问题。

由于我们所处的领域,敏捷方法才是我们的擅长,因而在这个象限中,我们推荐的书籍主要与敏捷相关。首先推荐的是一本祖师级的经典大作,Kent Beck的Extreme Programming Explained《解析极限编程》。一切关于极限编程的思想、原则与实践,都能追本溯源到本书,寻找到这些内容的出处。如果你从未曾了解或接触过XP,通过本书,可以给你最正确的引导;若你已经在项目中运用了XP,却又心存疑虑,阅读本书一定能为你答疑解惑。

相较于《解析极限编程》更偏重理论,Henrik Kniberg的著作Scrum and XP from the Trenches《硝烟中的Scrum和XP》会带给你不同的感受。我们喜欢它的短小,简单,实用,书的气质分外地符合敏捷的精神。该书是Henrik Kniberg真实项目经验的提炼,讲述了成功敏捷团队的工作过程,没有理论、没有引用、没有脚注、没有废话。它可以作为基础实践的入门指南,帮助团队正确实施Scrum与XP——但不能模仿。你需要了解自己所处的环境,进而对具体实践做出取舍,创造出属于自己的过程。

如今,精益思想已经被许多不同的行业所广泛采用,该思想在软件行业的影响尤为显著。现代化的软件开发思想和实践方法,早就开始从精益思想里学习和借鉴,包括迭代开发,质量内建,一人多技,一岗多能,全功能团队,看板管理,持续改善等等。精益思想背后的想法可以追溯到上个世纪40年代后期,丰田公司的一群工程师发现了在连续流动中进行少量生产的方法,这种方法可以像传统的大量生产者批量生产大量产品的方法一样有效率。对于初次接触精益思想的读者来说,这种”少量生产和批量生产一样有效率”的思想,未免有些反直觉和难以理解,因为我们从小接触到的观念就是”只有大批量生产才是有效率的“。Freddy Balle等人的著作The Golden Mine《金矿》就是为初次接触精益思想的读者准备的。本书采用小说的形式,描述了一家濒临破产的企业如何采用精益的方式,扭亏为盈,让人读起来非常轻松有趣。

Spray的模块结构

| Comments

Spray是基于Akka之上的轻量级Scala库,提供对客户端与服务端REST/HTTP的支持。在使用上,大概可以将其视为Java平台下的Jersey;虽然它们的特性并不尽相同。从Scala语言来看,它提供的Future与Promise可以较好地支持并发;而Akka的Actor模型无疑助推了Spray的并发处理性能。根据Spray官方网站的描述,它拥有如下的特性:

完全异步的,无阻塞的APIs
基于Actor与Future
高性能
轻量级
模块化
可测试性

之所以称其为库(library)而非框架(Framework),源于Spray的设计哲学。似乎在Spray的设计者眼中,只要沾上了“框架”这个名词,就显得不够轻量,耦合也会随之而来。虽然框架并非容器,无需管理诸多对象的生命周期,但它总会为应用添加一些框架级别的约束。框架更像是一个骨架(Skeleton),应用所要做的是将自己的内容填充到这个骨架中。应用能否运行良好,取决于框架的支撑作用。以我之浅见,大约可以如此简单分辨:框架之于应用,是一种白盒式的调用,你须得了解一些框架细节,遵循规定的约束或规范,才能更好地发挥框架的威力;而类库之于应用,则是职责向黑盒传递的委派,若要调用库,只需明晓其接口即可。

正如Spray的官方网站所说,Spray更像是基于Http搭建的一个集成层(Integration Layer)。因此你不必将自己的应用搭建在Spray之上,仅仅是当需要对Http的集成时,Spray可以成为你的一项选择。这种松散耦合,去除“供应商绑定”的方式,实在招人喜爱。Spray引人驻足的魅力,不仅在于Akka Actor模式对并发的良好支持,Scala语言本身也使其增色不少。Scala的语法较之Java更其灵活而先进,那些支持不变式、函数编程以及闭包等特性,总会让Java程序员垂涎三尺(至少在Java 8之前)。若再遥想不远将来Scala提供的async以及await(一如C# 5.0的语法特性),编写Scala程序应大有可为。当然,或也可能成亦Scala,败亦Scala,毕竟面对数量级庞大的Java程序员,Scala仍属小众。这会成为Spray的阿克琉斯之踝吗?

AgileChina2003架构演进杂志

| Comments

“架构演进”介绍

软件系统的架构从来都不是一蹴而就的,它需要在不断的演化中改进设计,甚至做出重要的架构迁移。尤其对于大型软件系统而言,组织管理、软件过程、需求变化、规模扩张、技术迁移、遗留系统等诸多因素都决定着架构的发展,甚至可能是诸多力量的博弈与权衡。

技术文章

演化架构与紧急设计系列

本系列文章旨在从全新的视角来介绍经常讨论但是又难以理解的软件架构和设计概念。作者 Neal Ford 将通过介绍一些具体示例来帮助您在演化架构和紧急设计的灵活实践中打下坚实的基础。通过将重要的架构和设计决定推迟到最后责任时刻,您可以防止由于不必要的复杂度而降低软件项目质量的问题。点击链接

遗留系统的技术栈迁移

遗留系统是一个还在运行和使用,但已步入软件生命周期衰老期的软件系统。它可能会因为无法满足新的质量需求,又或者是出于企业战略决策的考虑等诸多原因,需要对其进行技术栈迁移。然而,在迁移过程中,我们既需要满足迁移后的需求,又必须保证原有的系统功能不会受到破坏,这就为技术栈迁移制造了障碍。本文提出运用“风险驱动模型”来完成这一工作。风险驱动模型就是通过识别风险,对风险排定优先级;然后根据风险选定相关技术,再对风险是否得到缓解进行评估的一种架构方法。点击链接

注重实效的架构师——大胆行前人未行之路

是什么让架构师们精通自己的技艺?熟练的架构师是如何进行设计的?一次次,有人问起我这些问题,而我也不止一遍的问我自己。很明显,这并不只是软件工程过程、设计方法、技术或是编程的专业程度所决定的。很多架构师具备令人钦佩且完备的技术知识,这确实是使设计成功的必要条件。但是,还是有很多的软件项目失败了,或是在项目的架构中遭受到了严峻的挑战。掌握此道的关键在于架构师是以什么方式实现设计,他们重视什么,他们关注哪些方面以及在这些方面努力着。点击链接

代码是如何腐化的

| Comments

代码是如何腐化的?这是一个很大的话题,因为这种腐化的代码样本可能会体现不同的特征,若要彻底总结,可能会又是一本《重构》。我自然没有这个能力和知识。好在有一个简便的说法,即可以诉诸于“破窗理论”的威力。无论多少坏味道识别,重构手法运用,提高代码质量的最佳实践,以及运用诸多甄别代码质量体征的工具,都仅仅限于“术”的运用而已。若未能在开发人员内心树立整洁代码的习惯,时时刻刻对各种代码臭味保持敏感,且具有一颗期待卓越代码之心,那么,随着项目的演进,时间的推移,代码最终还是会慢慢腐烂。

这几日在开发一个User Story时,从诸多测试代码(包括集成测试与验收测试)中,观察到了一些接近腐烂的代码坏味。这些代码虽然不是产品代码,但同样是我们交付工件的一部分。最关键之处在于:它让我察觉到一种危险的趋势,若不能及时扭转,可能会让代码陷入腐烂的泥沼。若能及时解决这些糟糕代码,其实仅仅需要一些简单的重构手法,付出几个小时时间即可。

首先是针对集成测试的数据准备。我们要编写的集成测试针对Spring Batch Job,这些Job需要访问数据库,以验证Job的执行是否符合期望。我们发现在之前已有与Spring Batch Job相关的集成测试存在,并提供了访问数据库,以及启动、访问和停止Ftp服务器的功能。其中,与数据准备有关的功能放到单独定义的Fixture类中。这些Fixture是为特定目的编写的数据准备,可是,随着越来越多的Batch Job出现,有诸多集成测试都需要准备数据,于是开始慢慢产生了测试数据的重叠,逐步浮现出违背DRY原则的征兆了。