测试自动化后,我们需要怎样的QA?

我们先讨论一下在传统的瀑布模型下QA是如何工作的,其中最主要的问题是什么;然后作为对比,我们再来看看敏捷团队里的QA是如何工作的,工作重点又是什么;最后,我们详细看一看在新的职责下,QA应该如何做。

瀑布开发模型

即使在今天,在很多企业中瀑布模型仍然是主流。每一个需求都需要经过分析、设计、开发、测试、上线部署、运维等阶段。虽然一些企业已经开始实施敏捷开发,比如项目/产品以迭代的方式运作,也有诸如每日站会、代码检视等敏捷实践,但是如果仔细审视,你会发现其实开发模式丛骨子里来说还是瀑布:按照软件组件划分的部门结构(详见康威定律)、按照职能划分的团队(开发和测试分属不同部门)、过长的反馈周期、永远无法摆脱的集成难题等等。

waterfall-work

随着软件变得越来越复杂,团队里没有任何一个人可以说出系统是如何运作的,也不知道最终用户是谁,以及最终用户会以何种方式来使用最终的软件。

更糟糕的是,按照职能划分的团队在物理上都是隔离的,比如独立的测试部门,独立的运维部门,整日忙碌而难以预约到档期的业务人员,当然还有经常疲于交付,无处吐槽的苦逼开发。由于这些隔离,信息的反馈周期会非常长,一个本来很容易修复的缺陷可能在4周之后才会被另一个部门的测试发现,然后通过复杂的工作流(比如某种形式的缺陷追踪系统)流到开发那里,而开发可能还在拼命的完成早就应该交付的功能,从而形成恶性循环。

瀑布模式中的QA

在这样的环境中,QA们能做的事情非常有限。在需求开始时他们会参加需求澄清的会议,制定一些测试计划,然后进行测试用例的设计。有的企业会用诸如Excel之类的工具来记录这些用例。这些写在Excel里的,“死”的用例作用非常有限。而最大的问题在于:它们无法自动化执行。另外,在实际软件开发中,需求总是会经常发生变化,需求的优先级也会有调整,然后这些记录在Excel中的“死”的用例会很快过期,变得无人问津。

除此之外,QA中的有些成员会使用工具来录制一些UI测试的场景,然后在每个新版本出来之后进行回放。然而,当UI发生一点变化之后,这些自动化的用例就会失效:比如HTML片段中元素位置的调整,JavaScript的异步调用超时等等。

显然,这种单纯以黑盒形式来检查功能点的测试方式是不工作的,要真正有效的提升软件质量,仅仅通过事后检查远远不够,软件的质量也应该内建于软件之中。QA的工作也应该是一个贯穿软件生命周期的活动,从商业想法到真实上线,这其中的所有环节都应该有QA的参与。

系统思考

如果不从一个系统的角度来思考软件质量,就无法真正构建出健壮的、让业务和团队都有信心的软件系统。质量从来都不只是QA的职责,而是整个团队的职责。

team-spirit

关于软件质量,一个根深蒂固的误解是:缺陷在开发过程中被引入,然后在测试阶段被发现,最后在QA和开发的来回撕扯中被解决(或者数量被大规模降低),最后在生产环境中,就只会有很少的,优先级很低的缺陷。

然而事实上,很多需求从开始就没有被仔细分析,业务价值不很确定,验收条件模糊,流入开发后又会引入一些代码级别的错误,以及业务规则上的缺陷,测试阶段会漏掉一些功能点,上线之后更是问题百出(网络故障、缓存失效、黑客攻击、操作系统补丁、甚至内存溢出、log文件将磁盘写满等等)。

在一个敏捷团队中,每个人都应该对质量负责,而QA则以自己的丰富经验和独特视角来发掘系统中可能的质量隐患,并帮助团队将这些隐患消除。

circle-resized

我在ThoughtWorks的同事Anand Bagmar在他的演讲What is Agile testing- How does automation help?中详细讨论过这部分内容。

QA到底应该干什么?

本质上来说,任何软件项目的目标都应该是:更快地将高质量的软件从想法变成产品

将这个大目标细分一下,会得到这样几个子项,即企业需要:

  • 更大的商业回报(发掘业务价值)
  • 更短的上线时间(做最简单,直接的版本)
  • 更好的软件质量(质量内嵌)
  • 更少的资源投入(减少浪费)

其实就是传说中的多、快、好、省。如果说这是每一个软件项目的目标的话,那么团队里的每一个人都应该向着这个目标而努力,任何其他形式的工作都可以归类为“浪费”。用Excel记录那些经常会失效,而且无法自动执行的测试用例是浪费,会因为页面布局变化而大面积失效的UI测试也是浪费,一个容易修复的缺陷要等到数周之后才被发现也是浪费。

在这个大前提下,我们再来思考QA在团队里应该做什么以及怎么做。

QA的职责

Lisa Crispin在《敏捷软件测试》中提到过一个很著名的模型:敏捷测试四象限。这个模型是QA制定测试策略时的一个重要参考:

agile-testing-quadrants

如果按照纵向划分的话,图中的活动,越向上越面向业务;越向下越靠近技术。横向划分的话,往左是支撑团队,往右是评价产品。

其实简化一下,QA在团队里的工作,可以分为两大类:

  • 确保我们在正确的交付产品
  • 确保我们交付了正确的产品

根据这个四象限的划分,大部分团队可能都会从Q2起步:QA会和BA,甚至UX一起,从需求分析入手,继而进行业务场景梳理,这时候没有具体的可以被测试的软件代码。不过这并不妨碍测试活动,比如一些纸上原型的设计:

prototype-resized

这一阶段之后,我们已经有了用户故事,这时候QA需要和开发一起编写用户故事的自动化验收测试。当开发交付一部分功能之后,QA就可以做常规的用户故事测试了,几个迭代之后,QA开始进行跨功能需求测试和探索性测试等。根据探索性测试的结果,QA可能会调整测试策略,调整测试优先级,完善测试用例等等。

根据项目的不同,团队可以从不同的象限开始测试策略的制定。事实上,Q1-Q4仅仅是一个编号,与时间、阶段并无关系,Lisa Crispin还专门撰文解释过。

关于QA如何在软件分析的上游介入,并通过BDD的方式与业务分析师一起产出软件的各种规格描述,继而通过实例来帮助整个团队对需求的理解,ThoughtWorks的林冰玉有一篇文章很好的介绍了BDD的正确做法。如果将QA的外延扩展到在线的生产环境,制定合理的测量指标,调整测试策略,强烈推荐林冰玉写的另一篇文章产品环境中的QA

其他职责

事实上,软件生命周期中有很多的活动处于灰色地段。既可以说是应该开发做,又可以说应该QA做,甚至可以推给其他角色(比如OPs)。不过我们知道,一旦涉及角色,人们就再也不会按照全局优化的思路来应对问题了。这种灰色的活动包括:

  • 持续集成的搭建
  • 测试环境的创建与维护
  • UAT上的数据准备
  • 代码中的测试代码的维护
  • 测试代码的重构

在团队实践中,这些活动我们通常会让QA和开发或者OPs同事一起结对来完成。一方面避免知识孤岛的形成,另一方面在跨角色的工作中,也可以激发出更多不同的思路。

万能的QA?

虽然在这些活动中,QA都会参与,但并不是说团队里只要有一个QA就可以了。QA在参与这些活动时,侧重点还是有很大不同的。

c2_cargo

比如需求分析阶段,如果有QA的加入,一些从QA角度可以发现的有明显缺陷的场景,则可以在分析阶段就得到很好的处理。另一方面,尽早介入可以设计出更合理的测试计划(比如哪些功能的优先级比较高,用户会更频繁使用,那么对应的测试比重也会更高)。在Story分析与书写阶段,QA可以帮助写出更加合理的验收条件,既满足业务需求,又可以很好的指导开发。

在和开发一起编写澄清需求时,主要是编写自动化验收测试,而不是实际编写业务逻辑的实现(虽然QA应该参与Code Reivew环节,学习并分享自己的观点);甚至在上线运维阶段,QA还需要和OPs一起来设计用户数据的采集指标(比如用户访问的关键路径,浏览器版本,地区的区分等),从而制定出新的测试策略。

扩展阅读

Share

技术的执念

知识漩涡

只需稍加留意,我们就会发现自己被各种技术、工具包围。ThoughtWorks的技术雷达差不多每半年就会更新一次,在项目中更会遇到很多已经从技术雷达上消失的技术,项目上的旧技术/旧框架还在服役,新的技术/工具/语言/框架又在迅速的出现,有些昙花一现、迅速被新的后来者所取代。有的留下来了,不过也都在不断的演化、改变(不兼容的API,不同的版本等随处可见)。

如果你不幸是一个前端工程师,那么这个更新速度还要更加迅速。三年前Backbone是主流,两年前是Angular.JS,去年是React,紧接着Flux、Reflux等作为React的扩展而成为了新的主流;Grunt流行过一段时间,很快被Gulp替代,而新的Webpack又依稀有大一统的趋势。几乎每周都能看到新的框架涌现,双向绑定、虚拟DOM、事件代理、同构、后端渲染、更友好的语法糖、更快的执行速度等等,几乎任何一个方向都有无穷无尽的变化。

1-technology

而后端也好不到哪里去,容器技术、Web框架、ORM、构建脚本、自动化测试工具、依赖管理、应用服务器等等,你总有很多的选项,却又无法在事先区分到底哪个技术/工具更靠谱、更适合项目。

置身其中,往往有眼花缭乱、应接不暇的感觉。知识工作者当然需要终身不断的学习,但是像目前这种节奏,我很怀疑这是否是一种健康的状态。周围经常有人抱怨,好不容易上手了一个前端的MVC框架,一看周围的项目,大家已经在spike另外的框架/工具了(这意味着你在项目上无法使用该框架了……)。仅仅从学习的速度上来讲,我们已经远远无法跟上科技演化的节奏了,这是人类自身的一种限制。

知识的陷阱

假设你在一个Ruby项目上,学习了Rails/ActiveRecord/RSpec/MySQL。如果下一个项目还是Ruby,同样的技术站,你会觉得这是一种重复,因为除了业务逻辑、业务对象变化了之外,并没有新的内容,还是同样的技术。如果下一个项目是Python,技术栈变成了Django/nose/PostgreSQL,你可能会觉得有所提升,因为学到了不同的技术、框架、共建工具、测试工具等等,其实仔细观察,这还是一种重复,古人云:“换汤不换药”者,是也。

在目前我们所处的时代,信息以远远超过人们能接受的速度不断的被创造出来,一方面信息传播的速度大大提升了,另一方面是信息传播的渠道也极具多样化。我们无时无刻不被过载的信息包围着,即使你不主动的去尝试获取新的信息,手机App里的微信、微博、Flipboard、Pocket、知乎、开发者头条、Feedly、果壳、丁香园等等的推送已经足以提供给你足够的信息(大部分甚至都来不及消费就变成了历史信息而被忽略)。

以我自己为例,从2015年10月到2016年2月,我学习了很多东西,看一下下面这张图:

2-tech-tree

图中灰色方框中的内容是项目要求的知识,另外的则是我根据自己的兴趣学习的,两者基本上各占一半。事实上有很多内容(尤其是根据自己兴趣学习的)在真正要使用时,可能还需要学一遍。这些内容可能让我产生了我学到了好多东西的错觉。其实这个在另一个角度显现了技术人员的一个误区:以为自己可以掌握所有软件开发相关的知识(或者说太过于纵容自己的好奇心和兴趣)。

过载的信息

身处这样的信息过载环境,我们很难不为自己对信息的缺乏而感到不安,担心自己错过了什么重要的信息,这种担心和焦虑会促使我们进一步将时间消耗在对信息的获取上,从而更无暇思考什么是真正重要的。

《如何阅读一本书》将书分为两类:一种是提供资讯/信息(known)的,一种是帮助你理解(understand)信息的。相对于理解来讲,资讯本身其实并不那么重要。我们大部分人目前采用的碎片化的阅读方式无法提供给我们足够的“理解力”。我们都有这样的体验,有些书特别耗费脑力,读起来很累,而另一些书则非常轻松,易于消费。碎片化的阅读方式易于消费,只需要很少的思考就可以读懂,但是危害严重,它们并不会帮助你提升理解力。

4-fragmentation

但是直觉上我们会选择容易的事情来做,虽然这种浅层次的阅读只对扩展信息/资讯有帮助,对提升理解力则几乎无用。而我们在处理日常工作中的问题时,能真正帮助的,只有理解了的那部分知识。我在2014年,曾经有几个月屏蔽了所有微信、微博等内容聚合类的应用,也尽量少的去技术论坛,每天就是写代码,读纸质书,除了最初几天的忐忑之外,整个过程的收获非常大(而且也没有漏掉任何重要的信息)。

知识框架

技术人员有时候会有一种想要把所有技术都掌握的「执念」,这在局外人来看是一种荒诞不经的想法,但是置身其中,你很难看出这一点。毕竟,有意思的东西实在太多了,各种范式的编程语言、编译器技术、人工智能、数据可视化、地理信息系统、嵌入式设备、软硬件结合、大数据、自动化测试等等,每一个方向都有无穷无尽的有意思的东西。

但是在知识规模如此巨大的今天,一个人是无法掌握所有技术的,更不用说新的技术还在不断的涌现出来!这就要求我们有节制的来聚焦在某些技术上,而视其他技术如无物。当然这需要很大的勇气和魄力,不过唯有如此,技术人员才可能有真正的长进和成就。

我基于自己的经验,绘制了一个「Web开发」方面的知识框架,这个框架上包含了一个比较全的技能/知识集合,也是我认为一个「Web开发」人员应该掌握的一些知识点。

3-knowledge-framework

在成为一个专家之前,你需要先对要学习的领域有一个全面的认识。也就是说,做Web开发,需要尽可能覆盖到这个框架上的所有点。一旦完成了这棵树上的所有节点,就不用再去做第二次了,这时候你可以尝试找到树上的某一个分支,深入下去。这个听起来好像和我之前文章中的观点有所矛盾,其实不然。我在《我们真的缺前端工程师吗?》一文中提到过,“工程师不应该将自己束缚在前端开发上,要了解整个软件开发的全生命周期。”这里的观点其实是一致的,即首先要了解软件开发全生命周期中的所有节点,然后再有所侧重的去找自己的兴趣点来发展,即:先建立广度,再建立深度。

应对方法

对于知识的陷阱

当因自己的兴趣(而不是项目驱动,也就是没有实际的土壤来验证)而想要学习一个新的知识时,对照知识框架,如果发现自己已经在历史上学过它了,那就强迫自己放弃这个念头。比如你很熟悉用rspec来编写测试,忽然有一天心血来潮,想要学习JUnit,正确的做法就是泡杯茶,等这种冲动自己过去。相信我,一旦有了Java项目,你可以非常快速的掌握JUnit,而且很快会找到对应的feature,就像一个长期工作在Java技术栈上的同事那样!

对于过载的信息

实践中,首先要令自己相信:你无法掌握所有的知识,即使仅仅在软件开发领域。有了这个大前提之后,你只需要采取先建立广度,再建立深度的原则即可:

  • 做减法(在建立了知识框架之后,有针对性的学习)
  • 主动、深度阅读经典
  • 为那些有趣但非自己关注方向的知识赋予较低的优先级

另外,还可以尝试将微信、微博关闭一段时间,或者至少可以不去点那些朋友圈里的《老X聊微服务》或者《12个你不知道的Sublime技巧》文章,保持专注,保持简单。

Share

我们真的缺前端工程师吗?

前言

这两天在好几个地方都看到了一篇关于为什么整个互联网行业都缺前端工程师?的文章,文章本身是去年的,中心思想是:其实我们并不缺前端工程师,我们缺的是优秀的前端工程师。我还是比较同意作者观点的,不过略有意犹未尽的感觉。于是我结合自己的经验,也来聊一下这个话题:我们真的缺前端工程师吗?

These walls are kind of funny like that. First you hate them, then you get used to them.Enough time passed, get so you depend on them. That’s institutionalising.

传统软件公司划分开发者的方式下,在前端部门的程序员永远不会去读缓存数据部分的代码,设计师也不太可能去和开发坐在一起,开发也不知道软件最终软件会以何种方式部署在服务器上。

什么是前端工程师

我在招聘广告和办公室的一些对话中,听到了一个新的角色:UI Dev,事实上我在知乎上还回答过一个关于ThoughtWorks的UI Dev的问题。简而言之,UI Dev可以快速的把设计师的作品实现为HTML/CSS/JavaScript代码。

01

如果按照这个标准,我觉得UI Dev对自己的要求太低了。毕竟要学会HTML/CSS实现mockup并不困难,但是成为一名前端工程师则需要掌握更多的知识:

  • 会用PS来进行图片的处理(比如切图,微调等)
  • 用HTML/CSS实现mockup(可能还有SASS/LESS等工具)
  • 熟悉JavaScript(比如前端的MVVM框架,客户端模板)
  • 前端开发的工作流程(代码检查,精简化,模块化CSS,LiveReload,调试)
  • 编写测试(静态检查,单元测试)
  • 跨浏览器、跨设备的解决方法(不同分辨率,不同厂商)
  • 会根据项目的特点选择不同的前端技术栈(移动端,Web站点,响应式设计等)

在有了基础的HTML/CSS/JS技能之后,你会尝试做的更好:

  • 如何更高效的操作DOM
  • 如何将CSS写的更加清晰易懂
  • 如何编写更加易于维护的代码(更有意义的单元测试)
  • 如何组织大型的项目结构,模块化,组件化等等

这些要求事实上已经不那么容易做到了。它可能会花费你2到3年时间来完全掌握。但是2到3年之后,即便你已经成为了一个“合格的”前端工程师,这也还远远不够。在现实世界中,一个软件产品除了前端,还有非常广阔的空间,还有很多有趣的东西值得学习:

  • HTTP协议本身(缓存,鉴权)
  • Web容器/HTTP服务器如何工作
  • 无状态的Web应用的工作原理(如何让网站正确地运行在集群上)
  • 动态,静态内容如何分离部署(反向代理配置)
  • 安全机制如何配置
  • 监控机制如何配置

有了这些,也算是有点端到端的意思了。这时你也已经不是一个纯前端工程师了,系统中的大部分问题你都可以搞定,不过日常工作中可能更多的职责还是做前端的开发。但是这些还不够,软件除了交付之外,还有一些非功能性的需求:

  • 端到端测试(UI测试,比如selenium server/web driver)
  • devops(比如数据库环境,测试服务器,CI服务器的自动化provision)
  • 基本的UI设计原则(在某些页面确实的情况下,根据系统的已有UI做设计)
  • 数据库性能优化
  • 性能测试

这时候,你才能算是一个严格意义上的“前端”工程师。不从系统的角度来思考,不真正做一些后端开发/配置,并不能算是前端工程师,或者可以被称为偏前端工程师(partial frontend developer)。但是即使称为上边这样的“前端工程师”,我想这离一个优秀的工程师还是有很大差距的。

我跟一位设计师同事聊过这个问题:

Dev眼中的世界是这样的,从墙上(物理的或者电子的)上找到一些卡片(story卡或者需求文档说明书),然后撸袖子开干,干的过程中有很多自以为是的理解,同样有一些自以为是的牛逼实践(TDD啊,自动化啊),最后功能做完,大功告成,然后接着做下一个卡片。传统的Dev,或者苦逼屌丝程序员的世界就是这样的:需求从哪儿来,不知道;做完之后谁来负责质量,不知道;最终上线的时候怎么发布,不知道;线上有问题了怎么办,不知道。

在ThoughtWorks,Dev的工作有了很大的变化,一个最明显的变化是边界的模糊。比如很多项目都不设QA角色,所有人都对质量负责,都做测试,也有OPs角色,但是大部分非生产环境都是Dev自己发布。也就是说,软件/项目生命周期中的大部分实践我们都能涉足,而且可以带来改进,提升效率。但是这只是往下游(从开发,到测试,到部署,到运维),反过来看上游,比如需求从哪儿来,Dev还是不知道。这毫无疑问是一个令人沮丧的事实,因为这需求的产生才是核心,也就是我昨天跟你聊的:一个idea如何变成一个可视化的原型,然后进一步演进为项目原型?

开发工作不应该仅仅局限在编码上,作为开发者/工程师,应该尽可能的多了解一些上下文:比如我们的项目最终是给谁用的,需求从何而来,项目是如何部署在线上的等等。

02

software life cycle

简而言之,开发者视野应该放开开阔一些。不要将自己局限在某种角色上,不但不要局限在前端/后端开发上,压根就不要局限在开发这种角色本身上,你在系统中,可以是设计师,还可以是业务分析师。虽然不一定最终要你去转行做BA,或者UX,但是更广阔的视野可以使你更加高效的发挥自己的作用,也可以在和别的角色互动式,快速的了解上下文。

我所理解的,前端不一定要熟知所有这些知识和技能,但是一定不要认为自己做好了前端的一亩三分地就足够了,不要给自己设限。跨界会给你带来难以估量的好处,一个角色做久了,难免会产生一些盲点。这时候,换个视角,从其他角色的角度来看待你的工作,又会有很多新的发现。而且不仅如此,很可能你会发现之前很麻烦,很难搞定的事情,在新的方法/视角下变得很容易。

我的故事

其实,我是一名后端开发

工作之后,我在很长一段时间是专注于“非前端”的领域。和很多刚入行的新人一样,我对计算机能触及的几乎一切领域都感兴趣:语言解释器,人工智能(遗传算法,隐式马尔科夫模型,自动纠错,模式识别),嵌入式开发,图形处理,操作系统的进程调度,进程间通信,多线程模型,各种脚本语言(python,ruby,JavaScript等等),另外,日常开发流程中的一些工具的定制化也会花去我很多的时间,比如如何配置vim,写几个小脚本来和编辑器做集成等等。更别说那些令人一听就觉得激动的编程范式:面向对象,基于消息总线,函数式编程等等。如果你感兴趣,可以看看我几年前的博客

我的上一家公司的产品是一个省级电网的收费/计费系统(电其实和我们在超市里购买的其他生活用品一样,也是一种商品)。我在那里工作了差不多两年,日常的开发方式就是ssh登陆到RHEL(Redhat Enterprise Linux)服务器上,用vim(当然有一堆的vim插件)开发C代码,调试器是gdb(对,就是那个很牛逼,但是对新手特别不友好的gdb)。

我们用C语言给Apache的httpd写了一个扩展module,大约相当于现在rack里的中间件,这个module要和后端的一个要复杂的多的模块通信,其中不但涉及网络通信,还有*nix管道,缓冲,并发等等考虑。在这两年里,我几乎没有碰过任何的Web界面上的东西(处理用php写了一两百行的页面之外)在加入这家公司之前,我在一家用Java做报表的公司工作,技术栈为J2EE。其中有一些前端的工作,但是并不很多,而且说实话,我当时有些看不太上这些技术。HTML/CSS在我心目中的地位比线程池,语言解析等差远了,所以我也没有认真地去系统学习。

在加入ThoughtWorks之前,在“前端”方面,唯一算是比较擅长的也不过是写JavaScript,而且对于前端的MVVM框架,双向绑定,模块化等高级货都没听过。且不能论HTML/CSS的最佳实践,连根据设计稿做出一个静态页面的的能力也不具备。我之前有一点JSP/HTML经验,而CSS经验也并没有超出如何画一个细线表格的范畴。换句话说,我的前端(特别是HTML/CSS)是最近才学会的。

ThoughtWorks的开发

在ThoughtWorks,很多团队是按照feature团队来组建的。相对于传统的component团队(按部门划分,比如研发组,测试组,设计组等,每个组还有可能会再细分成如用户调研,流程设计,视觉设计等等),feature团队里配备了软件开发过程中需要的几乎所有角色:业务分析,测试工程师,开发工程师,设计师(设计师一般不会常驻),有的团队还有项目经理的角色。

在feature团队里,你可以很容易看到不同的角色是如何工作的,很多时候,开发会和设计师一起来调整颜色,排版,布局,也可能和测试一起编写自动化测试用例,showcase等。也就是说,角色之间的藩篱在淡化,而就开发这一种角色而言,对于前端/后端的区分也会显得非常模糊,因为需求划分之后的story(敏捷开发中的一个术语,其实就是需求的一种展现方式)是端到端的,比如一个商品列表展示的story,会包括

  • 数据库的表结构
  • 访问数据库的ORM部分,
  • 使用ORM的业务逻辑service
  • 响应客户端的controller(消费JSON或者XML的HTTP接口)
  • 发送请求,处理响应的JavaScript代码
  • 和设计稿一致的CSS样式

而且在这个过程中还会涉及到一些外围的工具

  • 虚拟机环境准备
  • 数据库连接
  • 自动化测试(单元测试,集成测试,可能还有UI测试)
  • 数据库迁移脚本

在这个过程中,开发者需要掌握和开发过程相关的一切实践中的一切工具.

在我的ThoughtWorks的第一个项目中,我是以Java开发工程师角色加入的,下项目的时候,我学会了自动化provision,cucumber测试工具,Rails,gradle(没错,我之前用Java都是用IDE构建的,在Linux世界我用make),jasmine测试工具,Backbone.js,haml.js。

第二个项目的时候,我是以前端工程师角色加入,下项目的时候,我学会了nginx配置缓存、负载均衡服务器,gatling测试工具,Hadoop/Spark等的集群配置,还有一些和项目相关的GIS(地理信息系统)的技术栈,前后端分离策略等。

第三个项目我是以Java开发工程师角色加入的,下项目的时候,我学会了如何做性能测试,如何建立一个漂亮的Dashboard(可以用来展现CI等),而且在业余时间系统的学习了CSS3和HTML5,将之前零敲碎打的那些知识串起来,这些总结做了几次内部培训后,还整理成了一本电子书

第四个项目我又变成了一个前端工程师,但是这个项目有意思的地方是跟mobile相关,于是页面性能,体验又变成了一个重点,下项目的时候,我对无状态的Web应用,session的持久化,CSS3的动画,用Backbone.js组织多页面的方式等等又有了新的理解。

如果这些经历造成了你觉得我很牛的错觉,那我应该道歉。我觉得自己勉强可以算是个合格的程序员:对学习保持着热情,对解决问题保持着热情,仅此而已。在项目上,如果我发现了问题,我就想办法解决,如果属于知识欠缺,那我就会去学习。我还远远没有到达精通这些技术的地步,但是在工程实践领域,根据80/20原则,这些粗浅的知识足以解决80%的问题,而另外的20%,我们才真正需要一个专家来帮忙。也就是说,团队里需要有一个能解决20%的问题的前端工程师,而其他的80%的前端工作,应该可以被其他所有的开发完成,对于后端开发也是一样。

在ThoughtWorks,经常可以遇到非常优秀的跨界人才:比如做出心声(一个帮助聋哑人打电话的软件)的朱晨,他的主要角色是用户体验设计师,为了开发心声这个软件专门学习了如何开发;比如西安办公室的大牛何飞,在学完了我上边提到Web领域的绝大部分技能之后,又开始学习各种底层算法;比如UX 王倩蕾唐婉莹,也在积极的学习前端开发;同样UX角色的刘海生,又在向BA角色进发。

总结

我们缺的从来都不是前端/后端工程师,而是工程师(或者那些会系统思考,并总是想着解决问题的人)。角色划分在大的机构内可能是有意义的,就像历史上工厂里,工人被分为车工,钳工,木工,电工。但是这种模式在软件开发中未必好用,具体而微的小团队可能更具竞争力。而在一个个的小团队中,再细分前端后端就显得比较滑稽了。团队中的每个成员都应该具备基本的端到端能力(不仅仅是开发,更应该是具有业务上下文,即每个人都清楚我们要交付的最终产品是什么,以及这个产品是如何帮助最终用户的),每个成员也都需要为最终的交付物负责,而不是为自己的职责负责。

感谢张霄翀唐婉莹为本文提供的feedback

Share

可视化你的足迹

数据可视化可以让读者以一种轻松的方式来消费数据,人类大脑在处理图形的速度是处理文本的66,000倍,这也是人们常常说的一图胜千言。在本文中,我们通过将日常中很容易收集到的数据,通过一系列的处理,并最终展现在地图上。这仅仅是GIS的一个很简单场景,但是我们可以看到,当空间数据和地图结合在一起时,可以在可视化上得到很好的效果,读者可以很容易从中获取信息。

01

我们在本文中会制作一个这样的地图,图中灰色的线是城市中的道路,小六边形表示照片拍摄地。颜色表示当时当地拍摄照片的密度,红色表示密集,黄色为稀疏。可以看到,我的活动区域主要集中在左下角,那是公司所在地和我的住处,:)

要展现数据,首先需要采集数据,不过这些已经在日常生活中被不自觉的被记录下来了:

数据来源

如果你开启了iPhone相机中的定位功能,拍照的时候,iPhone会自动把当前的地理信息写入到图片的元数据中,这样我们就可以使用这些数据来做进一步的分析了。

我在去年学习OpenLayers的时候已经玩过一些简单的足迹可视化,另外还有一篇全球地震信息的可视化,但是仅仅是展示矢量信息,并没有深入,而且都是一些前端的JavaScript的代码。最近又在重新整理之前的GIS知识,重新把这个作为例子来练手。当然,这次会涉及一些地图编辑空间计算的内容。

我的照片一般都通过Mac自带的Photos管理(前身iPhoto),手机里照片会定期同步上去。老版本的iPhoto用的是XML文件来存储照片的EXIF数据,新的Photos的实现里,数据被存储在了好几个SQLite数据库文件中,不过问题不大,我们只需要写一点Ruby代码就可以将数据转化为标准格式,这里使用GeoJSON,GeoJSON既可以方便人类阅读,也可以很方便的导入到PostGIS或者直接在客户端展现。

实现步骤

我们现在要绘制照片拍摄的密度图,大概需要这样一些步骤:

  1. 抽取照片的EXIF信息(经度,纬度,创建时间等)
  2. 编写脚本将抽取出来的信息转换成通用格式(GeoJSON)
  3. 使用QGIS将这些点的集合导入为图层
  4. 插入一些由六边形组成的图层(设置合适的大小)
  5. 计算落在各个多边形中的点的个数,并生成新的图层heatmap
  6. 使用MapServer来渲染基本地图

数据抽取

Mac上的Photos会将照片的元数据存储在一个SQLite3格式的数据库中,文件名为Library.apdb,通常位于这个位置~/Pictures/Photos\ Library.photoslibrary/Database/apdb/Library.apdb。这个文件可以通过SQLite3的客户端直接打开,不过由于可能有其他进程(Mac自己的)打开了该文件,所以会有锁文件,你可能需要先将这个文件拷贝到另外一个位置。

然后将表RKVersion中的部分信息导出即可,SQLite内置了很方便的导出功能,通过它提供的shell客户端sqlite3,将信息导出到csv文件中:

1
2
3
4
5
sqlite> .mode csv
sqlite> .headers on
sqlite> .output places-ive-been.csv
sqlite> select datetime(imageDate+978307200, 'unixepoch', 'localtime') as imageDate, exifLatitude, exifLongitude from RKVersion where exifLatitude and exiflongitude;
sqlite> .output stdout

注意这里的日期,苹果的日期偏移和其他公司不同,始于2001年1月1日,所以要在imageDate之后加上这个base值,然后将文件以.csv的格式导出到places-ive-been.csv中,该文件包含3列:时间,纬度,精度。

1
2
3
4
5
6
7
imageDate,exifLatitude,exifLongitude
"2012-10-25 16:34:01",34.19216667,108.87316667
"2012-10-28 14:45:53",35.1795,109.9275
"2012-10-28 14:45:45",35.1795,109.9275
"2012-10-25 16:34:04",34.19216667,108.87316667
"2012-10-19 23:01:05",34.19833333,108.86733333
...

转换为GeoJSON

方便以后的转换起见,我们将这个文件转换成GeoJSON(其实很多客户端工具可以支持CSV的导入,不过GeoJSON更为标准一些)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
require 'csv'
require 'json'

lines = CSV.open("places-ive-been.csv").readlines
keys = lines.delete lines.first

File.open("places-ive-been.json", 'w') do |f|
    data = lines.map do |row|
        {
            :type => "Feature",
            :geometry => {
              :type => "Point",
              :coordinates => [row[2].to_f, row[1].to_f]
            },
            :properties => {
              :created_at => row[0]
            }
        }
    end

    f.puts JSON.pretty_generate({
        :type => "FeatureCollection",
        :crs => {
          :type => "name",
          :properties => {
            :name => "EPSG:4326"
          }
        },
        :features => data
    })
end

这段脚本可以将我们的.csv转换成标准的geojson格式,注意此处的空间投影使用的是EPSG:4326

导入为QGIS图层

QGIS是一个开源的GIS套件,包括桌面端的编辑器和服务器端,这里我们只是用器桌面端来进行图层的编辑。

将我们的GeoJSON导入之后,会看到这样的一个可视化的效果!

02

我们还可以导入其他的地图图层,这样可以清楚的看到点所在的区域(国家地图图层可以在此处下载):

03

好了,有了基础数据之后,我们来作进一步的数据分析 – 即生成密度图。首先使用QGIS的插件MMQGIS生成多边形图层功能(Create -> Create Grid Layer),为了处理速度,我们可以将地图放大到一定范围(我选择西安市,我在这里活动比较密集)。

选择六边形hexagon,并设置合适的大小(如果是3857参考系,即按照公里数来设置,会比较容易一些,如果是4326,则需要自己计算)。简而言之,需要保证每个格子都包含一些点,不至于太密,也不至于太稀疏。

04

计算密度

QGIS提供了很多的数据分析功能,我们在这个例子中使用(Vector -> Analysis Tools -> Points in Polygon)工具,这个工具需要两个图层,一个是点集图层,一个是多边形图层。然后会将结果生成到一个新的图层中,我们可以将其命名为places-ive-been-density.shp,同时需要指定一个字段来存储统计出来的值(density)。

这个过程可能会花费一点时间,根据需要计算的点集合多边形的格式(也就是地图上的区域)。

完成之后会得到一个Shapefile(其实是一组,具体可以参看这里)。其实在这个过程中,绝大多数多边形是不包含任何数据的,我们需要过滤掉这些多余的多边形,这样可以缩减绘制地图的时间。

我们可以将这个文件导入到PostGIS中进行简化:

1
2
shp2pgsql -I -s 4326 data/places-ive-been/places-ive-been-3857-density.shp places_density |\
PGUSER=gis PGPASSWORD=gis  psql -h localhost -d playground

这里的shp2pgsql命令是GDAL工具包提供的命令,用以将Shapefile导入到PostGIS中,你可以通过

1
$ brew install gdal --with-postgresql

来安装。

GDAL会提供很多的工具,比如用来转换各种数据格式,投影,查看信息等等。

导入之后,我们可以在PostGIS的客户端查看,编辑这些数据等。比如在过滤之前,

1
select count(*) from places_density;

我们导入的数据中有103166条记录:

1
select count(*) from places_density where density IS NOT NULL;

而过滤之后,我们仅剩下749条数据。

通过GDAL提供的另一个工具ogr2ogr可以方便的执行过滤,并生成新的Shapefile:

1
2
3
4
$ ogr2ogr -f "ESRI Shapefile" data/places-ive-been/places_heatmap.shp \
PG:"host=localhost user=gis dbname=playground pass
word=gis" \
-sql "SELECT density, geom FROM places_density WHERE density IS NOT NULL;"

这条命令可以得到一个新的文件,这个就是最终的用来绘制地图的文件了。

绘制地图

开源世界中有很多的工具可以完成地图的绘制,比如MapServerGeoServerMapnik等等。我们在这篇文章中使用MapServer来完成地图的绘制,MapServer的安装和配置虽然比较容易,但是也需要花费一些时间,所以我将其放到了这个repo中,你可以直接clone下来使用。(需要你在虚拟机中安装ansible来完成provision)。

MapServer的配置很简单,类似于一个XML,不过是自定义的格式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
MAP
  IMAGETYPE      PNG
  EXTENT         11859978.732852 3994742.227345 12753503.595559 4580388.268737
  SIZE           8000 6000
  SHAPEPATH      "/data/heatmap"
  IMAGECOLOR     255 255 255

  PROJECTION
    "init=epsg:3857"   ##required
  END

  LAYER # States polygon layer begins here
    NAME         heatmap
    DATA         heatmap_3857
    STATUS       default
    TYPE         POLYGON

    CLASS
      NAME "basic"
      STYLE
        COLOR        255 255 178
        OUTLINECOLOR 255 255 178
      END
    END
  END

END

这些配置基本上都比较自解释,比如设置图片格式,图片大小,Shapefile的路径,图层的名称等,MapServer的文档在开源软件中来说,都算比较烂的,但是对于这些基本概念的解释还比较详尽,大家可以去这里参考

这里我们定义了一个图层,每个Map中可以定义多个图层(我们完成的最终效果图就是西安市的道路图和照片拍摄密度图两个图层的叠加)。

这个配置绘制出来的地图是没有颜色差异的,全部都是255 255 178。不过MapServer的配置提供了很好的样式定义,比如我们可以定义这样的一些规则:

  1. 如果密度为1,则设置颜色为淡黄
  2. 如果密度在1-2,则设置为比淡黄红一点的颜色
  3. 以此类推
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
  LAYER
    NAME         heatmap
    DATA         heatmap_3857
    STATUS       default
    TYPE         POLYGON
    #CLASSITEM density

    CLASS
      EXPRESSION ([density] = 1)
      STYLE
        COLOR        255 255 178
        OUTLINECOLOR 255 255 178
      END
    END

    CLASS
      EXPRESSION ([density] > 1 AND [density] <= 2)
      STYLE
        COLOR        254 204 92
        OUTLINECOLOR 254 204 92
      END
    END

    CLASS
      EXPRESSION ([density] > 2 AND [density] <= 3)
      STYLE
        COLOR        253 141 60
        OUTLINECOLOR 253 141 60
      END
    END

    CLASS
      EXPRESSION ([density] > 3 AND [density] <= 10)
      STYLE
        COLOR        240 59 32
        OUTLINECOLOR 240 59 32
      END
    END

    CLASS
      EXPRESSION ([density] > 10 AND [density] < 3438)
      STYLE
        COLOR        189 0 38
        OUTLINECOLOR 189 0 38
      END
    END

  END

这样我们的地图展现出来就会比较有层次感,而且通过颜色的加深,也能体现热图本身的含义。

同样的原理,如果将那些自己创建的多边形替换为行政区域划分的多边形,则可以得到另外一种形式的热图

05

总结

我们通过使用一些开源工具(MapServer,QGis,PostGIS,GDAL等),构建出一个基于GIS的数据可视化框架。在这个stack上,我们可以很容易的将一些其他数据也通过可视化的方式展现出来(公用自行车站点分布,出租车分布等等)。MapServer可以发布标准的WMS服务,因此可以很好的和客户端框架集成,从而带来更加友好的用户体验。

Share

技术人员如何写一本书?

我在过去的几年中,写了4本书。有传统意义上的两本实体书:《JavaScript核心概念及实践》《轻量级Web应用开发》,还有两本电子书《3周3页面》《函数式编程乐趣》。当然对我而言,主职工作是软件开发,写作是个副业。

在写作的过程中,有一些有趣的心得。

  • 写作本身是一个很好的学习过程(至少是一个驱动你学习的动力)
  • 写书非常枯燥,特别是校对的时候
  • 写作不会让你变得富有,但是有时候会让你开心(不总是)

写文章 vs 写书

写博客/文章和写书还是有很大差别的,一个明显的差异是写文章会比较随意,而且应该尽量保持精简。一篇文章提供一些信息即可,应该尽量远离细节(如果写一篇教程,则另当别论)。而写书则应该尽可能的深入细节,尽可能可以让读者依书自修。

投入与回报

首先要明白的一点是,不要指望用写书来赚钱,至少前4本是这样的。粗略的算一下:我的第一本书卖了3000册,每卖一本我可以得到4元RMB,一共就是12,000元RMB。而这本书我断断续续写了三年。那是很多个周末,很多个假期,很多个夜晚的付出换来的,如果真正要计算投入产出比的话(纯粹金钱上),这显然是一个毫不合算的事情。

作为一个参考,IBM developerWorks的投稿,千字200元,一般写5,000字以内,也就是800元RMB左右。而要写一篇这样的文章,我只需要一天(当然需要数周/数月的积累)。12,000元RMB需要写15篇文章,如果每周写一篇,不到4个月就可以写完,而且写文章比写书容易多了,毕竟篇幅比较短小,易于校对。而且对于大部分开发者来说,固定在一个主题上的难度要比15个独立的主题简单的多,因为无需特别深入。

所以根据经验,要抱着公益的情怀来写书。也就是说为了让知识更好的分享,让你学习到的先进科学技术来帮助更多的开发者,提高他们的开发效率,让他们可以在周末多休息一天。而至于翻译技术书籍,那基本上就是免费的了,完全是一个公益活动(耗时数月,斟酌字句,推敲表达方式,但是价格极为低廉:千字60元RMB),所以下次见了技术书籍的译者,就多少给他捐点吧,他们才是在为人民服务

知识的诅咒

“知识的诅咒”是指人们在获得了某种知识之后,就无法想象没有这种知识的情况了。这种现象随处可见,比如一个你到了一个从未去过的陌生城市,遇到以为当地人,然后向他问路。当地人觉得已经说的很清楚了,但是你还是不知道该怎么走。另一个例子是:假设你不认识泰文,然后你打开任何一本泰文写的小说,你只能依稀感觉到这是一种文字,除此之外你并不能从中获取任何的信息。但是当你学习了一段时间泰文之后,再来看这本小说,之前的那种感受就再也没有了。

curse-resized

写书的时候,你首先需要具备某种知识。但是写书的目的是将这些知识传递给那些不具备此知识的人,而根据“知识的诅咒”,你又无法确知那些初学者会遇到哪些问题!解决这个问题的方法就是找初学者来试读。而且为了保险起见,还应该找尽可能多的人来试读。

写作方式

一种方式是自下而上的,写一些独立的文章,最后发现可以串起来,然后形成一本书,另一种方式是自上而下,但是又会逐步调整。根据经验,不论是写一篇简单的博客,还是写一本书,都需要按照自上而下的方式。随心所欲的写下去,基本上都收不住,而且整个文章支离破碎,貌似有很多内容,但是不成章法,读者也无法轻松的获取知识。

先列出大的章节,然后逐步细化,但是未必是按照顺序来写。先编写自己最熟悉的部分,然后逐步完善。例子的选取需要精妙而恰当,最好有图例来说明。

配图制作

一般而言,我在书中会使用两种图:流程图和一些截屏。截屏通常使用Mac OSX自身的功能就已经足够,而流程图我会采用一些额外的工具如:

  • graphviz
  • keynote/sketch

cgi

用Graphviz画图的好处就是可以将图像代码一样放入版本库来管理。

除此之外,我还学习了一些设计软件的基本用法,事实上只需要用一些简单的元素就可以做出非常专业的配图:

  • 字形/字体(大小,粗细的变化)
  • 颜色(基本的配色理论就可以做出很舒服的配色)
  • 层次(尺寸,位置,颜色的深浅)
  • 阴影

mock-server-resized

代码格式

书中实例需要很多代码来说明,如果是制作电子书的话,可以使用Markdown预处理器自带的功能来高亮。另外如果需要RTF格式,可以使用这些工具:

  • highlight工具
  • intelij中的插件copy on steriod

这里有一篇博客来说明如何将你的代码带着格式拷贝到剪贴板中,拷贝之后,就可以将这些内容粘贴到Word或者Keynote中了。

jest.dontMock('../components/headline.jsx');

describe('Headline', function() {
  var React = require('react/addons');
  var Headline = require('../components/headline.jsx');
  var TestUtils = React.addons.TestUtils;

  it('#render', function() {
    var text = "this is a title";
    var headline = TestUtils.renderIntoDocument(<Headline title={text} />);
    var title = TestUtils.findRenderedDOMComponentWithTag(headline, 'h4');
    expect(headline.getDOMNode()).toBeDefined();
    expect(headline.getDOMNode().textContent).toEqual(text);
  });
});

一些潜在的坑

在写作的过程中,会有一些潜在的坑。这些所谓的坑是新人可能无法想到的。相对于言之无物,不知如何下笔,最痛苦的其实在于平淡。大部分时候,你可能很容易就能写出开头,但是很难坚持到最后。即使好不容易写完了第一版,后续的重读和修改,会让你苦不堪言。

内容写好之后,样式是下一个重要的问题,好的内容需要有与之匹配的排版。在中国,作者不但要负责内容,还要负责一些排版的事情。这一点非常奇葩,但是又是实情。这也是我更推荐电子版的原因(排版更加美观,选择更加多样,而且一旦有问题可以更容易的修改)。

另外一个问题是错别字检查!检查错别字对于作者来说,是一件非常困难的实情。而对于读者来说,则是一件很容易的事情。这跟知识的诅咒的道理一样。

typo

发布方式

实体书

传统的出版方式有一些明显的问题,这些问题已经和现代的知识传递方式产生了冲突:

  1. 时滞性(新技术的更新速度远远超过审批,印刷等流程的时间)
  2. 排版(如何低成本做到语法高亮,或者彩图)
  3. 更新频率(当技术更新之后,如何更新,是传统纸质书无法解决的问题)

传统的出版方式有点像传统的软件开发,一本书从开始写作到最终出版,要经过很多环节。忽略掉写作过程,从交稿到出版会经历很多次审核和校对,可能会历时4-8个月,着这个过程中,很多东西都可能发生了变化,一个典型的例子是《用AngularJS开发下一代Web应用》,原版为英文版,翻译成中文版再到出版之后,书中的很大一部分内容已经过时。读者拿到书之后,会发现书中的内容已经和当前的版本/文档不匹配了。这种现状随着技术的更新速度和频率还会再加剧。

第二点是排版。我听说国内有些出版社已经开始接受Markdown作为稿件的格式,但是大部分还采用Word或者WPS等格式,这样排版就变成了一个大问题。以我自己为例,我的原稿用Markdown写,但是写了几章之后不得不切换到Microsoft word上,而我自己的Mac OSX下的排版到编辑的Windows下就会变样,而且还会涉及字符集,字体,Word版本等等问题的影响,最后导致印刷出来和原始稿件出入很大。

最后一点是更新频率,如果发现了错别字或者错误的地方(即使之前检查过多次,仍然会有漏网之鱼),由于实体书的特殊性,一般需要等到再次印刷才能解决。这意味着先购买的读者会承担一些风险,更新后的版本又如何让读者看到呢?总不能又买一本吧。

但是这些问题都可以通过电子书来很好的解决。首先,电子书可以随时更新,最低限度的降低时滞性。排版上来说,作者可以使用Markdown来编写,而展现则可以应用一些预定义的模板来完成。最后,更新频率完全可以控制,对读者来说风险更低,因为电子版书籍的可以很容易的追踪交易记录,从而得到免费的更新过的版本。

电子书

目前已经有很多的渠道可以发布电子书,比如gitbook知笔墨。这些应用的出现,大大降低了发布书籍的成本,我的《函数式编程乐趣》,用了3天就完成了草稿,而发布只需要数秒。

另外一个问题是书籍的价格和作者的收入。一本书定价50元RMB,出版社给作者的版税是8%,也就是说,没卖出一本,作者可以得到4元,如果你的书非常畅销,这还是一个不错的价格。但是可能90%的书籍都不会是畅销书(就好比每个班级都会有优等生,但是他们仅占全班人数的10%一样)。这对作者是一种浪费:你需要耗时数月甚至数年来写一本书,然后市场的反馈又非常慢(毕竟你无法出版一本未完成的书)。

我在selfstore.io上有两本电子书:《3周3页面》《函数式编程乐趣》《3周3页面》定价为16元,每卖出一本,扣除掉交易费之后,我可以得到14.7元。

对我来说,这样可以得到更多的回报,对于读者则可以更加快速的得到更新,而且由于有预览版和一系列的其他信息,还可以在很大程度上降低读者的风险(更不用说快递费,等待时间等问题)。我在gitbook上的统计显示,《3周3页面》已经被累计下载了28,861次,实际的读者也将近5,000。而且没有任何的审核流程,也没有排版的时间浪费,我只需要关注内容即可。

Share