7个你需要知道的结对礼仪

结对编程远远不是两个程序员坐在一起写代码那么简单。 — 鲁迅(没有说过)

结对编程

结对编程可能算是比测试驱动开发更具有争议的敏捷实践了,事实上,仅有很少的团队可以很好的实施它并从中受益,对于更多的团队来说,即使在践行敏捷的团队中,也常常会分为旗帜鲜明的两个阵营。提倡者往往会强调结对编程在“传递领域知识”,“减少潜在缺陷”,“降低信息孤岛的形成”等方面的作用;而反对者则认为结对编程在很多时候都是在浪费时间:开发者在实践中很多时候都难以聚焦,容易产生分歧,另外个人的产出往往也难以度量。

这篇文章不打算讨论结对编程对效率的影响,也不讨论要不要进行结对编程,当然也不会涉及以何种力度来执行结对。这里的假设是团队决定采用结对编程,但是对于如何实施存在一些疑虑;或者已经在采用结对编程的团队里,发现很多时候结对编程并没有很好的发挥作用,需要一些更有实践意义的指导。

结对编程远远不是两个开发者坐在一起写代码那么简单。作为一种科学且充满趣味性(才不是)的工程实践,事实上它是有一些基本原则的,团队里的开发者需要正确的实施这些原则,结对编程才有可能为团队带来实际的受益。

假设你找到了另一个程序员,并且已经准备好一起来编码实现一个具体的业务需求。在开始着手开始结对之前,你首先需要setup环境。

硬件设置

工欲善其事,必先利其器。首先你和你的peer需要一个大的外接显示器和有一个可以调节高度的桌子(当然也可以用纸箱子DIY一个低配版)。这一点往往被初学者忽视,而事实上它可以直接决定结对能不能作为一个可持续的团队工程实践。如果你的颈椎长时间处于不舒服的状态,你的注意力很快会退散,精神会变得难以集中,而一个处于病态的身体是无法支撑长时间的编码工作的。

在开始之前,请将屏幕调整到舒适的高度,以不仰头不低头为度(身高不一样的两个人可以通过调整椅子的高度达到基本一致)。此外还要注意看屏幕的角度,如果你需要抻着脖子才能看清楚屏幕,那么当时间稍长一点时,你的颈椎也会非常吃力。因此,如果条件允许的话,你和你的peer可以使用双屏来进行结对。

当然,还有一些常见的其他关于硬件准备的细节,比如:

  • VGA/HDMI转接头
  • 充足的电源
  • 键盘/鼠标转接头
  • 便签和笔

纸和笔永远是你(们)的好朋友,在实际动手写代码之前,请拿出纸笔来将要做的事情划分成更细粒度,可以验证的任务列表,贴在显示器的下边缘。最后,另外记得将手机调成震动模式,利人利己。

软件设置

硬件准备好之后就可以进行软件的配置了。软件问题远比硬件复杂,因为大部分开发者都有自己钟爱的工具集,小到curl/wget,大到Vim/Emacs,不要期望在这个问题上和peer达成一致,这是可遇而不可求,可望而不可即的。

根据我自己的经验而言,高级一些的IDE比如JetBrains出品的Intellij,WebStorm等都可以随意切换快捷键集合(keymap)。如果你和peer就快捷键的使用上无法达成一致,在轮到你输入代码的时候,你可以切换到自己熟悉的Keymap,反之亦然。

在Intellij/WebStorm里,你可以通过ctrl+来切换各种设置,比如选择3就可以切换不同的Keymap,然后在下一步的窗口中按照自己的喜好进行切换预设的Keymap。某项目的王晓峰(Vim党)和张哲(Emacs党)尤其擅长此技,他们两人结对时,可以做到在抢键盘的瞬间将keymap切换到自己的挚爱而对方无察觉的地步。

这种技法事实上仅仅对于结对双方都有很高超的键盘操作能力的前提下,也唯有这种场景下,双发可以互不妥协。对于另一种场景(可能在实际中更为常见),比如结对一方键盘技巧和操作效率低下,影响结对流畅程度的场景,则需要效率低下的一方自己摒弃陋习(使用鼠标而不是键盘),快速赶上。

和别人结对之前,你需要至少熟悉一个IDE/编辑器,比如通过纯键盘的操作完成

  • 按照名称查文件
  • 按照内容搜索
  • 定位到指定文件的指定行/指定函数
  • 选中变量,表达式,语句等
  • 可以快速执行测试(可以在命令行,也可以在IDE中)

熟常基本Shell技能和常用命令行工具的使用,可以完成诸如

  • 文件搜索
  • 网络访问
  • 正则表达式的应用
  • 查找替换文件中的内容

等操作,这样在结对时可以大大提高效率。这些都是稍加练习就可以掌握的技能,并没有多少技术含量在内,而且学会了可以收益很久。

当知识不对等时

好了,铺垫了这么多,终于到了正题部分。最理想的结对状态是,双方的技能水平相当,知识储备基本类似,可以非常流畅的进行交流,在结对过程中可以完全专注在需要解决的问题本身上,讨论时思想激烈碰撞,编码时键动如飞,不知日之将夕。这种场景下完全不需要任何技巧,随心所欲,自由发挥即可。与此对应的另一种场景是双方都没有任何储备,技能也无法胜任,这种情况我们需要在项目上完全避免。

这两种极端的情况之外,就是不对等的场景了,这也是现实中最为常见的case:很多时候,结对双方会有一个人比较有经验,而另一个人则在某方面需要catchup。比如一个老手带一个新手,或者一个擅长业务的开发和另一个该领域的新人结对等。

一般而言,需要双方有一个人来做主导,另一个人来观察,并在过程中交互,答疑解惑,共同完成任务。与传统的教与学不同的是:结对需要的是两个智慧头脑的碰撞,而不是单方面的灌输。因此观察者不是单方向的被动接受,主导者也并非完全讲述。事实上结对是一个会有激烈交互的过程。

主导者

对于主导者来说,千万不要太投入,而无视peer的感受。这种场景非常常见,我自己有时候也会不自觉的忽略掉peer,自顾自的写代码,很多时候把peer当成了小黄鸭。这时候你的peer会有强烈的挫败感,也很难跟上你的节奏,从而影响结对的效果。作为主导者,需要更耐心一些,不断的和自己的peer交互。

另一个极端是,主导者太热心的coach,而忽视了给新人实际锻炼的机会。这时候需要主导者给peer更多的实践机会:比如在带着新人编写了一个小的TDD循环(红绿重构)之后,可以抑制住自己接着写的冲动(我知道这个非常困难),然后将键盘交给你的peer,让他模仿你刚才的做法来完成下一个。

有时候,当你看到peer正在用一个不好的做法来完成任务时,你可以即使让他停下来,并通过问问题的方式来启发他:

  • 还有更好的做法吗?

如果peer仍然在迟疑的话,你可以进一步提示:

  • 你觉得XXX会不会更好?

一个实际的例子是,你们在写一段JS代码来迭代一个列表,你的peer正在用for循环来操作一个数组,而你可以提议使用Array.map。有些时候,你的peer会给你一些惊喜的回答!他的回答甚至比你预想中的更加出色,你也可以通过这种方式来向他学习。

观察者

另一方面,作为观察者而言,结对毋庸置疑是一种特别好的学习机会,你应该抓住一切可能的机会来向你的peer学习。包括快捷键的使用,命令行工具参数的应用,良好的编程习惯等等。保持你的专注力和好奇心,比如你看到peer神器的通过快捷键删除了花括号(block)中的所有代码,或者将curl的返回值以prettify过的样式打印到控制台,或者通过命令行merge了一个PR等等。

在实践的时候,可以采取Ping-Pong的方式来互换主导者和观察者的角色。比如,A写一个测试,B来写实现,A来重构,然后换B来写测试,A来实现,B来做重构等等。开始时,可能会由一个人来主导,随着合作越来越顺畅,你们可以提高交换的频次。

保持专注

在选定了要两人一起解决的问题之后,你们需要一起完成任务划分。这样可以确保你们可以永远关注在单一任务上,避免任务切换带来的损耗。

在做完一项任务后,用mark笔轻轻将其从纸上划掉(或者打钩)。千万不要小看这个小动作的威力,它既可以将你们的工作进度很好的表述出来,也可以在任何时候帮助你们回到正在做的事情上(特别是在吃完午饭之后),另外这个微小的具有仪式感的动作是对大脑的一个正向反馈,促进多巴胺的分泌(代码写的这么开心,还要什么女朋友?)。

很多时候,我们需要暂时搁置争议,保持前行。

无法统一的意见

如果你遇到了一个固执己见的同事,而不凑巧的是你也是一个难以被说服的人,那么如何处理那些无法避免的争论呢?特别是那些没有对错之分的技术问题。比如那种编程语言更适合Web开发,比如如何践行TDD(比如自顶向下的TDD和自底向上的TDD)等等。

有时候,我们会非常坚持自己觉得对的东西,觉得那就是真理。挑战这个真理的不是傻就是二,但是用不了多久,我们就会发现,换个角度好像也说得通,特别是在和其他人结对,并突然意识到以前的那个完全无法接受的做法似乎还是有几分道理的。

在我刚做完的一个前端项目中,做技术选型时我自然的选了更早项目中使用的scss module,而团队里的另一个同事则提议使用styled-component。我们谁也没有说服谁,最后写代码的时候就有两种风格。直到有一天,我在代码库里看到了用styled-component写的很漂亮的组件,我自己尝试着把相关的scss重写成styled-componet,结果发现确实比单独的scss文件要更好维护一些,而且也不影响既有的测试。

我突然意识到,我所坚持的只是一个假的“真理”,先前的坚持和做技术选型时的理由就变得很可笑:那只不过是为了使用自己熟悉的技术而编造的理由而已。保持open mind是一件知易行难的事情,希望大家在争辩时能念及这个小例子,可能会少一些无谓的争辩。

对于这种难以统一意见的场景,我建议可以将其搁置,先按照某一种提议进行,知道发现明显的,难以为继的缺陷为止。往往你们会发现一条比较折中的路,或者一个人被另一个人说服。

棘手的任务

即使很有经验的程序员也会遇到新的领域,或者在熟悉的领域遇到新的困难。有些情况下,作为结对的两个人都对要完成的主题没有头绪。这时候非要挤在一起反而会降低速度,无助于问题的解决。

一个好用的实践是,两人分头研究,并严格控制时间。比如Time box 30分钟。不过很可能在30分钟后,你们中至少有一个人已经对要怎么做有了头绪,如果30分钟还没有头绪,则可以求助团队其他成员。

比如我在最近项目上遇到了Kerberos认证的问题,我和peer都没有接触过,在经过20分钟的独立spike之后,我发现了一篇细节很丰富的,看起来很靠谱的技术博客,而我的peer则在内部github上找到了另一个团队的可以工作的代码(虽然代码质量不是很好)。我们最终决定copy+paste,然后做重构的方式继续前进。而那篇技术博客则是一个很好的课后学习的资料。

张弛有度

注意力是一种非常稀缺的资源,普通人很难全神贯注在某件事情上超过30分钟。一旦超过这个时间,大脑就开始偷懒,开小差。这时候一个短暂的break可以让大脑得到很好的休息。人类的大脑有一个非常有趣的特性,就是它的后台任务处理能力 — 而且后台处理能力好像远远强大于前台。你可能会在去冲咖啡的路上,突然灵光一闪,那个困扰你多时的问题有了思路,而此时此刻的你的大脑明明在想如何用咖啡机冲一份拿铁。

如果遇到一个难以理解的bug,或者在设置测试环境是遇到了困难,休息一下很可能帮助你找到解决问题的新角度。为了避免长时间纠缠在冥思苦想中,你和你的peer可以采取比如番茄工作法之类的时间管理工具:

  1. 从Todo列表中找出下一个任务
  2. 设置一个不可中断的25分钟,开始工作
  3. 时间到了之后,休息5分钟
  4. 重复2-3,4次之后休息15分钟

这里有一个在线工具可以直接使用 ,你也可以用手机的闹铃工具。

结对轮换

如果结对的对象长期固定的话,pair本身又会变成新的信息孤岛。比如A和B长期负责订单模块,而C和D则一直在写门店管理,那么毫无疑问,一段时间后,A和B就不知道C和D在做什么了,不论是领域知识还是技术实践,都很难得到有效的知识传递。当一个团队规模变成10+之后,这还可能演化成更为严重的项目问题。

因此需要定期或者不定期的轮转,比如一周轮换一到两次,A和C来写订单,B和D来写门店管理,这样可以保证领域知识,工程实践,工具的使用等等知识都很好的在团队内部共享。

在一些场景下,团队采取前后端分离的方式进行开发。前端和后端的技术栈选择大相径庭,每一端都有不同的约定和复杂的配置,这会对结对轮换的实施造成障碍,而且短期来看还会影响开发效率。如果团队再大一些,DevOps可能会独立出一个小组来负责,这将导致结对的轮换更加困难。

在实践中,我发现让不同角色的团队成员轮换结对所带来的好处(伴随着短期阵痛的)远胜过知识的隔离带来的坏处。团队中的前端开发如果花费一些时间和DevOps一起结对,他会对系统的整个架构更加清楚;而后端开发和DevOps结对则可能让他意识到代码中的潜在缺陷和解决方法(比如会话外置,缓存策略等)。

尊重

作为一个最小单位的团体活动,你常常要站在你的peer的角度来看问题。如果你不愿意和某一特性的人结对,那么首先不要让自己成为那样的人。比如你讨厌只闷头写代码,不理会peer有没有跟上的那种结对方式;又或者你不喜欢和用鼠标完成又低效又别扭操作的人一起写代码(我在和这样的人结对的时候,都需要费很大力气抑制自己,才不会从peer的手中把鼠标抢过来扔掉),那么首先让自己不是那样的人。

除此之外,尊重还体现在很多其他细节中。当你不得不中断结对而去做其他事情时,务必让你的peer知道。而且在离开之前,你应该表示歉意,不要凭空消失,然后若干分钟后又凭空出现,没有人喜欢和一个不靠谱的人工作。比如10点30分的水果时间到了,你看到有人拿着你喜欢吃的桃子从厨房方向走了回来。你可以示意peer暂停一会,然后去厨房拿点水果,记得给你的peer也带上一些。

另一方面,当你的peer回来之后,你需要及时和他catchup,告诉他你正在做什么,已经做到了哪一步等等。快速的将他带入到上下文中。

控制情绪

情绪是一件非常微妙的事情,它具有很强的传染性。当你们的工作任务收到各种blocker,被各种其他事情干扰而导致进度难以推进时,一定要注意自己情绪的控制。如果你的peer一直在旁边唉声叹气,或者抱怨连连,你会变得非常沮丧,并且很难集中精力在积极解决问题上。

你可以通过积极的寻求外部帮助,或者将blocker更快的可视化出来,让团队了解,并提供可能的帮助。

课后练习

和你的peer完成了充实而卓有成效的一天之后,你需要总结一下自己记录的知识点,这是一个绝佳的提升自己能力的方法。通过实战,发现自己的缺点,并通过近距离观察别人如何解决该问题,最终会以很深刻的印象记录下来,这时候针对性的查漏补缺是可以取得非常好的效果。

比如你已经习惯使用grep来做搜索,结果你的peer娴熟的awk技巧使你打开眼界,你可以在课后专门学习一下这个工具的各种选项,并尝试熟练应用。或者你们在代码库中探索到webpack的一些特殊配置,它可以良好工作,但是你不是很明白背后的原理,这些都可以放在结对结束之后自己消化。花一些额外时间来更新你的技能可以让你在第二天的结对中更加得心应手,也可以更好的融入到结对编程带来的快乐中。

这些结对的基本礼仪,都是一些微小的细节,做好了可以让和你一起工作的人比较舒服,也会帮助你自己建立一个更加高效的工作环境。

小结

在这篇文章中,我总结了一些有关结对编程的常见的问题和解决方法。在开始进行结对之前,首先需要确保硬件设施正确setup,这样可以保证大家可以在很轻松舒适的环境中工作。在软件设置上,保证效率的前提下,可以有不同的偏好设置。当能力不对等时,恰恰是结对编程最能发挥作用的场景,不但对于观察者来说是绝好的学习过程,对于主导者而言,也可以从coach过程中看到一些不同的东西。

在结对编程过程中,你们需要始终保持专注,可以通过任务拆分的方式来帮助一直关注在单一事项上。此外,应该有定期的休息,让紧张的情绪得到缓解。为了避免大尺度上的信息孤岛,团队还需要定期的进行Pair的轮换。

总而言之,通过这些方法的使用,可以有效的促进工作效率,促进个人成长为前提,并和可以形成很好的团队氛围。


更多精彩洞见,请关注微信公众号:ThoughtWorks洞见

Share

你要专业

在我不算长的职业生涯中,有很多同事都给过我正面的评价(当然,可能有更多负面的评价,不过我都选择性的遗忘掉了)。有人欣赏我的命令行技巧,有人则称赞我代码写的比较快,有人说我的Vim的插件配置的很高效,还有人说坐在我旁边写PPT效率会变高。而我自己最喜欢一个评价是:有我在项目上的时候,团队就会觉得安心。

我喜欢这个评价是因为它是我希望自己能展现的一个状态:成为一个靠谱的职场人。关于靠谱的职场人,《重来》的作者之一Jason有一个很精炼的描述:

Work ethic is about showing up, being on time, being reliable, doing what you say you’re going to do, being trustworthy, putting in a fair day’s work, respecting the work, respecting the customer, respecting the organization, respecting co-workers, not wasting time, not making work hard for other people, not creating unnecessary work for other people, not being a bottleneck, not faking work. Work ethic is about being a fundamentally good person that others can count on and enjoy working with. — Jason Fried

大意是:职业精神就是可靠,言出必行,尊重工作,不要浪费时间,不给别人添麻烦等等。不过,精炼的描述往往失之可操作性不够强。就好比知道“高内聚,低耦合”并不能帮助你写好代码一样。在这篇文章里,我打算举一些日常工作中常见的例子,尝试通过Specification By Example的方式给靠谱的职场人下一个定义。

在职场中,靠谱的意思就是:成为一个别人会信赖并乐于和你一起工作的人。而人们喜欢和靠谱的人一起工作。

尊重你的工作

人们站在不同的立场看待同一件事情时,必然会产生分歧。这几乎在项目中随处可以见,比如对于某个Feature,客户认为必须实现,这样在年终的绩效考核中才有令领导满意的成绩;另一方面,对于交付团队来说,技术上难以实现或者工作量太大,难以在有限的时间内完成;开发团队钟爱新的流行的技术栈,而客户则相对保守,需要考虑培训成本等等其他方面的因素。如果再加上沟通不畅,很容易导致团队士气低落,甚至双方互相抱怨。

一份工作,既是企业(企业中的员工)赖以生存的条件,又是企业和服务对象共同为社会创造价值的载体。这个产品可以解决某些人的问题,提高效率,为社会带来价值。工作本身就是值得尊重的。如果你被指派到一个项目上,并且作为个人,你不认为这个项目违背了自己的价值观,那就应该全力以赴去完成它。另一方面,如果你认为项目的价值和自己的三观不匹配(比如,如果有人出钱让你去开发GFW之类的工具;或者一个工具用来监控员工的桌面等),你完全可以选择不去参加这个项目。

新人常犯的一个错误是将遵循既有规则当成尊重。举个例子,在实际交付的时候,团队中的Tech Lead做出了一项你任务是错误的决策,作为团队成员,你可以据理力争,表达自己的看法,并提出自己的提议。另一方面,你也可以假装这个决策是超出你控制范围的,并且假装它是正确的,然后低头来按照决策来实施。在我看来,第二种看似尊重的行为是对团队和项目的非常的不尊重。退而言之,即使你的提议被拒掉,你也可以从中学到很多之前看不到的知识。一方面可以帮助你自己理清思路,看到自己方案的缺点,另一方面,你可以学习人们在实际项目中,如何对于不同的条件来做妥协。

与他人合作

一个人可以走的更快,一群人可以走的更远 — 非洲谚语

我们的日常工作中,总避免不了要和不同角色,不同经验的人一起工作。和单枪匹马独自作战不同的是,在一个群体中,个人的行为需要遵循一定的约定,需要和其他团队成员一起配合来完成任务。换句话说,要有团队合作精神。在和其他人一起工作时,第一要义是不要为别人制造更多的工作量。事实上,你应该在自己能力允许的前提下,尽可能让别人做最少的工作。相信我,你会希望自己和这样的人合作的。

我们从一个假象出来的场景出发,来看看如何在类似的场景中展现专业性:

CI服务器地址

设想这样一个场景:你所在的团队有一个微信群,大家很多问题都在群里提出并讨论。但是一些简单问题可能被频繁的问道,比如XXX服务器的地址是啥?前两天又有一个新人加入了团队,他今天在群里问了一个之前被回答过好几次的问题:谁知道CI服务器的地址,请发给我一下,多谢!

而这时候,你可以做什么呢?“我之前发过邮件给所有人了,你翻一下邮件”。

这是一个可行的方案,但是需要

  • 邮件标题的关键字 或者
  • 邮件正文关键字

而要做搜索本身动作,他需要打开邮件客户端或者在浏览器打开Web Mail,如果是访问Web Mail,他可能还要登录公司内网(输入密码),还可能要查看Okta推送的消息,这又需要打开手机,切换到Okta,点击确认。如果工作环境网络有vpn的隔离,情况可能会更复杂。

如果你花费1分钟帮他搜一下,然后把地址发给他,并告诉他用户名/密码就是域账号的用户名/密码,效果则会好很多。

访问外部API

如果我们把这个场景在稍微复杂化一点:有人在微信群问如何在Postman中访问获取所有网点信息的API。要访问这个API,客户端需要一个Endpoint和一些特定的HTTP Header。最简单的做法是发送一个截图。

不幸的是,Header中有一个叫做x-api-token,它的值是一个256个字符的hash。这时候图片就变得毫无用处了:你不会期望问问题的那个人用手敲一遍token吧?另一个方法是把API的Endpoint和所需的HTTP Header是分别以文本形式发送给他,(如果这个API需要多个header的话,你可能要复制粘贴好几次)。

再进一步,你可以通过一个cURL加命令行参数的方式将这些内容一次性发送给他:

$ curl -H "x-api-token: token" -H "Accept:application/json" https://host:port/top-security-resources/1

嗯,挺不错的。不过如果明天又有其他人问你要这个API的访问方式,你又要再来一遍,还是比较麻烦。你稍微翻了下Postman的帮助,发现它支持导出,还可以定义一些环境参数等。如果将这些内容都导出出来,然后放在代码仓库中,其他所有人就无需每次都找人要各种URL了。

显然,最后一种方法既满足当前的需求,又有很好的可扩展性,这样你就通过一个具体的问题,抽象出一个高阶的问题,并且为这个问题提供了一个可行的解决方案了。当然,这种方法的缺陷是会占用你很多时间,需要你学习额外的知识。不过,如果是我,我肯定愿意选择这种方式。

帮助团队里的测试

我记得我们曾经有一个研发平台项目,其中一个需求是实现租户代码坏味道识别的工具:这个工具的输入是平台上程序员提交的源代码,然后我们的工具会分析类和类之间的关系,然后给代码评定一个分数:比如集成层次不能太深,不能有多继承之类。代码本身并不难写,但是要测试时需要列举很多case,每一个case至少需要可以能编译通过,这要求测试同事还要会写合法的C++代码,也就意味着他们还需要C++的多继承,抽象类之类。

我花了一些时间(两个小时左右)为测试同事写了一个小工具:通过指定一些参数,比如类的继承层次,是否多继承之类,然后这个工具帮你生成一堆合法的可以编译通过的源代码:包括多个文件,文件之间的引用(比如头文件中定义接口,然后在实现代码中访问这些接口等等)。测试可以很容易通过它来生成测试用例,然后再来验证待验证的工具到底能不能识别这些坏味道。

自动化工具

另外一个有趣的例子是:很久前的一个项目,团队在开发一个大表单,大表单里有很多问题,大概分为三个页面:第一页需要填写一些个人信息,比如姓甚名谁家住何处等,第二页则会填更多的信息,而第二页的很多问题跟第一页相关,一些问题只需要在第一页选了A选项才会出现,而另外一些问题则仅仅为B场景设计等等。

然后某个新需求是在第三页添加一个新的问题(根据第二页的某个回答来决定要不要显示出来)。在实现过程中,开发人员需要手工填很多内容才能到达第三页,而这个过程还可能出错(比如第二页中需要去调用某个API来生成下拉列表内容,那个API有可能会挂掉),一出错又得重来一次(当时还没有HMR啊,State管理啊这些高级货,只能重新刷新)。团队意识到这个痛点之后,有人就开发了一个Chrome的扩展,这个扩展可以根据预设的答案自动填写表单(好像是模拟鼠标点击的方式),直到你想要停下来的那一个问题。这样就将开发中调试的时间大大缩短,还可以节省很多测试同事的工作量。

事实上,这类的场景在实际项目里会有很多。通过一些自己的努力,让团队里的其他角色、或者团队之外的你的下游系统、又或者未来的系统维护者的生活变得轻松一些,是靠谱的一个重要表现形式。

做好Desk Check

在敏捷开发中,当一个Story开发结束之后,我们会把开发,测试,BA,UX聚在一起来做Shoulder Check / Desk Check。这个实践可能很多团队都会坚持。但是做的流畅程度则千差万别,效果也自然大相径庭。

我现在还记得第一次做Desk Check时候的忙乱,由于事先没有准备好,当围观群众上来之后,我和peer还没有把Jira上的卡打开。当逐条过验收条件(AC)的时候,我们才发现漏掉了一条。然后在跨浏览器检查的时候,发现在Safari里页面上有个按钮在点击时毫无响应,这时候我和peer打开了dev-tools开始了现场debug等等。

在后来的项目中,我特别注重这个实践,努力让Desk Check变得流畅无碍。你前期准备的越充分,在Desk Check的时候就越顺畅。比如,在把所有人都召集过来之前,自己先把所有AC过一遍,如果有Feature测试的话,就把测试用例大概过一遍,看看能不能覆盖所有AC。在Check的开始前,先把Story描述一遍,特别是业务场景,业务价值。这个过程可以对着Jira卡来过,如果有新的讨论,也需要顺手同步到Jira的comments里,以便未来参考。由于参加Desk Check的QA和BA可能手头都有很多任务并行处理,所以你需要快速的将上下文分享出来,让大家在同一理解水平上,这样后续的Check才有可能顺畅。

如果Story涉及跨浏览器,那么你最好可以将各个浏览器都打开,而且切换到需要showcase的页面上。这些前期的准备工作,可以减少参与者的上下文切换成本,可以让大家迅速进入到验收中,而且出错甚少。事实上,大部分问题在你的准备中都已经解决了,剩余的小问题则可以在后续的Story开发过程中顺手修复。

测试数据准备

BA对系统的理解是基于现实世界中的业务来的,因此在数据准备上一定要小心,比如10位数字的身份证号码,带有字母的手机号等等。即使是测试数据,也应该认真对待,尽量避免使用随机的文本来填写表单,否则结果页面看起来会非常不专业。

在有些场景下,比如你需要测试当文本超过一定长度会显示省略号,你仍然需要仔细设计文本,让其看起来更为实际。当你读到一个名称为“锟斤拷锟斤拷锟斤拷…”的产品时,你会做何感想?

事实上,一些工具可以帮你简化测试数据的准备,而且可以确保专业性。比如Ruby里的Faker(Perl中的Data:Faker的Ruby移植版)https://github.com/stympy/faker,它可以帮你生成很多常见的数据实例,比如

require 'faker'

Faker::Name.name      #=> "Christophe Bartell"
Faker::Bank.iban #=> "GB76DZJM33188515981979"
Faker::Internet.email #=> "kirsten.greenholt@corkeryfisher.info”

使用类似的工具,只需要编写一些微小的代码,就可以生成更贴近业务场景的测试数据,从而让显得更加专业。

设计中的细节

同样的道理,UX在设计稿中,需要考虑很多细节,比如

  • 常见的拼写错误
  • 业务术语的正确使用
  • 理解数字的含义
  • 视觉一致性(字体的选用,相同元素的字号,颜色暗示等)

这些细节可以让观众体会到你的认真和用心,而由于视觉的特殊性,一个微小的纰漏都可能被放大成严重的问题。比如在某一份设计中,所有的标题都采用24号深灰色的Consolas字体,但是在另一处,字号变成了18,而且加粗了。这种错误很容易别识别,从而让人产生不好的印象。

业务语言

而对于用错术语,或者没有完全理解业务时,对数字的解读则会产生更严重的问题。比如在广告行业,广告主比较关心的一个指标是Frequency,计算Frequency的方式是用PV(Page View)/UV(Unique View),也就是每个用户的平均点击量。页面的总访问量肯定比访问这些页面的人数要多(抽屉原理),那么它们的比值也肯定会大于1。如果UX不理解这个业务含义,可能会在设计稿上标识0.68之类的数字。

类似的,如果你在绘制一个Pie Chart,那么最起码所有部分之和加起来要等于100%,而且大致的占比要正确,比如应该避免出现:数值为25%但是视觉上比例却接近1/3等等。

对开发者友好

此外,在设计稿中,能为不同的场景设计出相应的变体(variation)也会大大降低开发者和UX之间来回讨论的工作量。比如在一个产品列表的设计中,列表中的第一个条目展示正常情况(happy path),而第二个显示当某些元素缺失时的展现(空值,非法值等),而第三个条目显示当标题超长之后是应该折行还是显示省略号等。

每一个细节事实上都在为你的靠谱程度打分,也会潜在的影响别人是否愿意信赖并乐于与你一同工作。

小结

文章中列举了一些实际项目中的例子,有关于如何做好开发实践的技巧,有关于帮助团队里的其他人更方便工作的意识,有关于对开发者友好的设计细节。所有这些例子中的技巧,事实上都与这样一个事实有关:要在职场中成为一个靠谱的人,意味着即使对团队内部,你也需要扮演一个专业服务者的角色。你需要更多的站在他人的角度来考虑问题,在合理的范围内,尽量的减少别人和你合作时的工作量。此外,你需要处理好很多细节,职业性体现在很多的细节中,从测试数据中的asdfasdf,到设计稿中的typo,都可能暴露你是否在用心对待工作。

文中提及的这些值得践行的技巧事实上与具体技术关联甚弱,你可以很容易的举一反三,并在实际场景中灵活运用,成为一个专业而靠谱的职业人。


更多精彩洞见请关注微信公众号:ThoughtWorks洞见

Share

如何成为优秀的程序员?

作为一个从业快10年的程序员,我想给新入行的程序员们一些建议。这些建议是我希望自己可以在毕业时就读到的,也希望它们可以帮助你成为一个更好的程序员。

简单归纳一下,总共有7条:

  1. 保持健康
  2. 编程之外的爱好
  3. 持续学习
  4. 正确应对犯错
  5. 不要囿于角色
  6. 展示你的创意
  7. 刻意练习手速

下面我来详细说说每一点。

保持健康

三寸气在千般用,一旦无常万事休。

——《金瓶梅》

首先要说的当然是健康,脱离了这个本钱,一切都无从谈起。

久坐、不运动、睡眠不足、不注意及时补充水分、长期的伏案工作等都会对健康造成很大的影响,而不幸的是,程序员这几样全都占了。很多程序员往往年纪轻轻就已经有了各种各样的疾病:颈椎病、腰椎间盘突出、高血脂/高血压、胆结石、腱鞘炎等等,关于程序员过劳死的新闻更是隔一段时间就来刺激一下我们的神经。

研究表明,长期保持同一姿势(不论坐着还是站着)对身体都有不同程度的害处,而且这种害处是无法事后弥补的。也就是说,如果白天上班坐8个小时,那么就算你下班后去健身房练一个小时也于事无补。这几年很流行的“站立式办公”也是一样,如果你白天站立时间过久,会对膝关节造成较大的压力,同样会损害健康。比较推荐的方式是,写30-40分钟代码就起来走一走,喝杯水,远眺一会,跟同事聊聊天。

我知道,作为程序员我也常遇到那种写代码写High了连厕所也不想去的时候。不过为了长远的健康,还是要养成良好的习惯。

戒除不良习惯

除了长时间保持同一姿势之外,许多程序员还有各种不良习惯。比如:

  • 吸烟
  • 喝酒
  • 嗜糖(碳酸饮料,其他高糖饮料)

这些习惯一般都会被美其名曰提神,大家都知道,程序员加班在业界算是比较常见的,萎靡不振是常态。然而这些号称提神的方法,其实没有一个是真正管用的。这些不良习惯说到底都是一种“毒瘾”,跟吸食大麻在本质上并无二致。不过好消息是你完全可以戒除这些不良习惯,只需要坚持一段时间,让“毒瘾”过去就好了(和真正的毒瘾一样,它们更多的是精神依赖,一旦你战胜了自己对它的精神依赖,就可以获得自由)。

我在大学和刚开始工作的前几年,也有烟瘾。写代码写累了就会去办公室外边冒一根,那种一氧化碳中毒带来的短暂微醺感确实令人有放松的错觉,但是抽完烟回来写代码会感觉更累。而且口中老感觉有异味,咽喉不适,最主要的是精神萎靡,终于有一天我受不了了,决定戒烟(事实上和很多人一样,之前也有过无数次的戒烟)。当烟瘾发作的时候,我就去喝杯水,晚上则站站桩(站完之后口齿生津,神清气爽)。刚开始的3天是最难的,一周之后我基本可以控制住去抽烟的欲望,然后就越来越轻松,完全感觉不到烟瘾对我的影响了。

碳酸饮料、高糖饮料也是一样。在饮食本来就不充裕的自然界,我们的祖先遇到了富含可以为身体提供能量的糖(比如蜂蜜),自然会大量摄入。这种嗜糖的基因在今天还在不断的产生作用,但是不同的是,我们现在可以很轻松的在食物、饮料中摄入比身体所需多得多的糖。这些糖会给健康带来很多问题,比如肥胖,高血糖,冠心病等等。

更多时候,我们想要喝饮料更多的是精神上的依赖,也就是上面说到的“毒瘾”。戒除对糖的依赖比烟和酒要困难一些,因为生活中有很多陷阱,比如酸奶、面包、饼干、水果等等。

零度可乐的陷阱

现在香烟的包装上印有焦油含量,有10mg的、有15mg的。焦油含量是影响一支烟口感的重要因素,通常说的“绵”其实是说焦油含量较低,这会让你感觉比较健康。然而陷阱是,一支烟抽完觉得不过瘾,神经感受到的刺激不够强烈,这会驱动你抽第二支,结果吸入的焦油反而更多。本来15mg焦油的一支烟就可以让你过瘾,现在两支10mg的才能达到同样的效果,相当于摄入了20mg。

零度可乐也是一样,那种无糖的有着甜味的添加剂会刺激你对糖的渴求,你需要摄入更多的糖来抵消这种虚幻的渴求,然后变得更不健康。

有人可能会说,没有这些嗜好,那活着有什么意思呢?相信我,当你戒除了这些“毒瘾”,有了一个健康的体魄,才真正能体会到活着的乐趣。当你为这些嗜好所控制,产生的那种病态的舒适感其实是虚无缥缈的。

一些建议

有规律的做一些运动可以缓解颈椎、腰椎的不适,可以加快新陈代谢的速度,消耗多余的、会沉积下来的能量。比如比较容易接触到、也容易上手的运动:

  • 瑜伽/普拉提
  • 乒乓球
  • 跳绳

选择一个适合自己的运动方式,然后将其培养成一个习惯(比如坚持每周两次瑜伽,或者每天中午打30分钟的乒乓球)。如果这些和工作有冲突的话,比如公司要求长期晚上加班,那你可以考虑换一家公司。

培养一个编程之外的爱好

如果让不同的人对程序员打标签并排序,一定会排在前三。在任何的聚会上,程序员总是很容易被识别出来的:聪明、戴眼镜、话不多、略显闷骚、聊天容易冷场等等。也难怪,长期钻研技术,沉浸在非黑即白的二进制世界,爱刨根问底,这样很容易把天聊死。

我建议新手程序员可以找一个编程之外的爱好,一来可以拓展自己的社交圈,周末可以有个不一样的过法(而不是宅在家里写代码);二来可以帮助你成为更好的程序员。

你肯定有过这样的经历:一个编程问题一直困扰着你,试了很久都找不到解决方法,结果出去散了会儿步,或者和别人唠家常,突然脑海里灵光一闪,想到了问题的答案。事实上,我们大脑的工作方式就是如此奇妙,换一个完全不同的上下文就可以让大脑得到很好的休息,而且往往可以产生1+1>2的效果。写代码写累了去听听音乐,或者打一会乒乓球就可以很好的缓解疲劳,甚至可以打开思路,产生新的灵感。

一些建议

学习一项与编程无关的技能,比如:

  • 乐器(如吉他,架子鼓)
  • 绘画(素描,水粉,水彩等)或者书法
  • 制作美食
  • 某一项武术(拳击,泰拳,空手道等)

这些看似毫不相干的爱好可以帮助大脑休息。另外需要注意的是,你无需真正成为某一项爱好的专家,不要有额外的压力:担心演奏不好、没有绘画天赋等等。没关系,它只是一个爱好而已。

我自己就尝试过很多不同的爱好,比如素描、书法等。

持续学习

软件开发是一个需要终身学习的行业(其实如果你不想做那种混吃等死的人的话,基本上每个行业都是这样)。我毕业的时候,SSH(Spring Struts Hibernate)是Web开发的主流,jQuery则是前端的新锐。有一些企业开始尝试AdobeActionScript,不过这个语言很快就消逝在了人们的视野中。基于jQUery,但是融入了MVC理念的Backbone.js提供更高级的抽象能力,成为了开发“大型”前端应用的首选;紧随其后的,大而全的Angular.js则通过内置的双向绑定、依赖注入、完善的测试支持等让前端开发变得和后端开发一样健全;再后来虚拟DOMReactive范式React栈则又一次颠覆了前端的开发方式。虽然现在还不知道下一次的颠覆会在哪里发生,但是可以肯定的是它一定会发生

除了基础框架之外,各种构建工具也是层出不穷,从最早和后端放在一起的mavenrake,到基于NodeJSgrunt,再到gulp,到webpack,最后又回归到npm script

程序员被裹挟在技术演进的洪流中,不能自已。作为程序员,你不但要非常扎实的掌握基础知识(操作系统原理,计算机网络,数据结构,算法等),还需要有非常强的快速学习能力,以及愿意不断去学习的态度,而后者可能更重要。

一些建议

  • 读书
  • 通过视频/文本教程等学习新技术

建议新手每天抽出一个小时来读书,周末可以多读一些。ThoughtWorks有个读书雷达,是一个很不错的书单,包括了很多的经典书籍。读书之外,还可以在线学习一些教程,比如TutorialplusEgghead等,都非常值得经常去看看,如果有比较新鲜有趣的技术,不妨自己亲自动手试一试。

关于英文能力

毫不夸张的说,英文能力是优秀程序员和普通程序员的华丽分割线。有了好的英文能力,可供你学习的资料库会立刻扩大数百甚至数千倍:海量的优质免费教程,视频,和优秀的中文教程一样,它们都深入浅出,通俗易懂,风趣幽默,只不过中文的会比较少,而且一般总是会滞后于英文版本而已。

英文能力不但可以帮你熟悉各种前端库、CSS框架等的介绍。还可以让你学习世界各国程序员对各种库的测评、框架的使用心得、踩过的坑等等。

我在2012年加入ThoughtWorks的时候,面试时磕磕绊绊的说不出话来。等到6个月试用期结束的时候,已经可以出差去澳洲和客户的OPs谈笑风生了。2013年的8月,在印度普内,我已经可以用英文给来自世界各国的学生讲课。

除了更顺畅的和不同文化的人交流、讨论问题之外,可以明显感觉到学习的速度变得更快,更有效率。

我自己实践过的一个比较有效的方法。我每天会花两个小时(早晚各一个小时)看澳洲之音上的视频,我会听写出视频中的每一句话,如果听不清就重复,有的句子可能会重复十遍。听到最后,视频中的每句话我都能听懂,而且能一边听一边写出来。这样坚持了差不多3个月,我基本上就可以听懂客户的需求澄清,开会的时候也可以比较完整的听明白每个人讨论的点。

其实诀窍就是坚持,这3个月中,每天两个小时,我没有一天间断。过了这一关之后,就很容易了,尽量多听多说就好。

另一个提高的方法是翻译书,我更建议你跟另外一个有经验的同事一起翻译,大家互相监督,也有个照应,比较不容易半途而废。

正确应对犯错

斯坦福大学的Carol Dweck教授通过一些实验和后续的研究提出了很有名的心智模型(Mindset)理论,简而言之,她发现不同的人们对待失败这件事有着完全不同的态度:有一类人害怕失败,失败后会变得不能接受,而且容易否定自身并影响进一步的尝试,Dweck教授称这类人为固定型思维模式(Fixed Mindset);而另一类人则“喜欢”失败,视失败/犯错为学习的一种方式,他们更关注过程而不是结果,Dweck教授称其为成长型思维模式(Growth Mindset)。

Dweck在演讲中提到,通过向成长型思维模式的转变,关注从失败/犯错中学习,人们的潜力可以得到很好的发挥,也更容易获得理想的结果。

很多新人不敢尝试,又不愿意让同事知道自己的不足,这样的态度会导致他更倾向于选择更容易的工作,这样就可以避免暴露自己的不足,久而久之就会形成恶性循环。其实企业对于新人的期望一般都不会很高,对于新人犯错也是有容忍度的,新人要勇于承认自己的不足,勇于尝试新的事物,勇于犯错并从中学习。

承认自己的不足在刚开始是一件很困难的事情,不过在尝试过几次之后,你就会发现其实也没有那么恐怖。你慢慢会喜欢那种不带任何包袱的、全身心聚焦在学习本身上的快乐。

不要被角色限制

都梁在《血色浪漫》里有段描述陕北农民的文字:

钟跃民惊讶地发现,在如此贫困恶劣的生存状态下,村民们却很少愁眉苦脸, 他们始终很乐观,他们最喜欢谈论的话题是饮食男女。在饮食方面,由于他们没见过更好的食品,所以坚持认为酸汤饺子和油泼辣子是天下最美味的食品,如果有人提出世上还有很多更好吃的东西,那大家会一致认为此人太没见过世面,这八成是没吃过酸汤饺子,才在这儿胡咧咧.

就像酸汤饺子并非天下最美味的食品一样,开发也不是世界上最牛逼的工作。任何一个良好的,健康的产品、项目都需要不同的角色共同配合,共同努力。如果仅仅将自己局限在程序员这一角色,时间久了未免会有坐井观天的狭隘。

作为程序员,既可以往上游去探索需求的梳理,用户痛点的分析,业务价值的挖掘,又可以向下游如测试的编写,产品的发布,运维监控。视野开拓了,才有可能对产品有整体的了解,也更容易在程序员这个角色上做的更好。

作为新人,能在自己擅长的方面发挥长处当然很好,但是如果仅仅局限在自己擅长的方面则未免太过单薄。如果你在前端非常有经验,那么除了将这些经验和知识分享给别人之外,你还可以向别的角色学习他们擅长的技能,比如向测试学习自动化、SBE等;向后端学习高性能,高可用服务器的技术、数据库设计及优化、API设计等;向DevOps学习运维技能,自动化provision技能等等。

这些不同的技术不但可以让你的视野更加开阔,也可以为自己以后尝试不同的角色和机会打好基础。以我自己为例,我刚工作的时候是一个Java开发,后来开始做产品的前端开发。换了工作后又跑到Linux下用C写服务。再后来加入ThoughtWorks后,正经职位是开发,不过在项目上还兼职过一段时间QA,在有些项目上,当UX不在场的时候还可以做些简单的设计,在技术社区当讲师,还在一些客户现场做过咨询顾问。我自己觉得在不同的角色上切换非常有意思,我自己也很享受整个过程。

展示你的创意

将一个创意、复杂概念或者想法简洁而准确的描述出来是一个非常重要的能力。我见过太多的程序员都是沉默寡言,讲东西声音又小,又紧张,即使有很好的想法也难以完整的表述出来。

不过这个能力是可以锻炼的,只需要借助原型的制作就可以了:

  • 画图
  • 静态原型
  • 纸上原型

俗话说,一图胜千言。你只需要学习一些简单的绘画技巧就可以大大提高自己的表述能力。

通过用静态页面(HTML/CSS/JS),mock数据等方式,快速的将创意表达出来是程序员的一个优势,你可以用静态数据、数据文件等方式,通过一些简单的代码快速的作出可以做交互的原型,然后通过和用户不断确认的方式来渐进增强,这种做法可以避免太大的浪费,尽早的将客户价值交付。

原型并不局限在草图,可以工作的静态页面,还可以是一个清晰简洁的演讲。基于PPT的原型还可以用来分析目前产品痛点、对比方案的优劣、展示自己的看法等等。

纸上原型是另一种低成本,可供快速交流沟通的原型方式:

(图片来自我在ThoughtWorks的同事刘海生)

手速

关于程序员是否要求很快的手速是一个颇具争议的话题。支持者认为这属于基本功,每个程序员都应该打字都很快;反对者则认为程序员的价值在于思考并解决问题,追求速度快,那还不如招个打字员。我个人的观点是,好的程序员应该有很快的手速(包括打字的速度,但不局限于此)。

我在ThoughtWorks西安办公室组织过很多次提升手速的工作坊,比如三周三页面闪电计划等。基本原则就是对一个具体的“作业”,不断的重复练习。

最近带两个新人,我给他们布置了一个简单的作业:

图片来源:dirbbble.com

基本要求是以最快的速度实现这个页面,并有一点微小的交互(比如选择联系人之后的checkbox会显示选中状态,剩余invites的数量减少等)。第一次做他们用了5个多小时(连同搭建环境,安装Node.js,npm包等),第二次用时2个半小时,第三次用时1个半小时,第四次用时50分钟。

对同一个页面的不断练习听起来是在做重复工作,其实可以联系到很多的内容:

  • 命令行的熟悉程度
  • 快捷键的使用
  • 搜索引擎的使用
  • Stackoverflow的使用

当你真的可以熟极而流的时候,你才有时间来考虑如何优化,比如如何抽取模板工程(这样下次做同样的事情就会快很多),如何精简DOM结构,如何用命令行工具来帮助自己提速等。手速是大前提,没有速度,一切优化都是脑海中的意淫,无法真实落地。

总结

要成为一个厉害的程序员,首先当然是要有一个好的身体。此外需要培养一个编程之外的爱好,这样可以让你活的像一个正常人(而不是传统的工科书呆子)。程序员是一个需要不断学习,不断充实的职业,在学习的过程中,英文能力可以帮助你学的更快,更有效,另外正确的应对学习过程中必然会犯的错误,并将每次错误都当成学习的机会。

开发只是软件开发流程中的一环,程序员需要拓展自己的视野,和其他角色一起配合才能保证产品的交付。在日常的开发中,程序员还需要快速、准确的将自己的想法和创意表达出来。最后,更快速的完成手头的工作可以让你有更多的时间来思考,来改进那些低效的工作方式。

扩展阅读


更多精彩洞见,请关注微信公众号:思特沃克

Share

从三明治到六边形

软件项目的套路

如果你平时的工作是做各种项目(而不是产品),而且你工作的时间足够长,那么自然见识过很多不同类型的项目。在切换过多次上下文之后,作为程序员的你,自然而然的会感到一定程度的重复:稍加抽象,你会发现所有的业务系统都几乎做着同样的事情:

  • 从某种渠道与用户交互,从而接受输入(Native App,Mobile Site,Web Site,桌面应用等等)
  • 将用户输入的数据按照一定规则进行转换,然后保存起来(通常是关系型数据库)
  • 将业务数据以某种形式展现(列表,卡片,地图上的Marker,时间线等)

稍加简化,你会发现大部分业务系统其实就是对某种形式的资源进行管理。所谓管理,也无非是增删查改(CRUD)操作。比如知乎是对“问题”这种资源的管理,LinkedIn是对“Profile”的管理,Jenkins对构建任务的管理等等,粗略的看起来都是这一个套路(当然,每个系统管理的资源类型可能不止一种,比如知乎还有时间线,Live,动态等等资源的管理)。

这些情况甚至会给开发者一种错觉:世界上所有的信息管理系统都是一样的,不同的仅仅是技术栈和操作的业务对象而已。如果写好一个模板,几乎都可以将开发过程自动化起来。事实上,有一些工具已经支持通过配置文件(比如yaml或者json/XML)的描述来生成对应的代码的功能。

如果真是这样的话,软件开发就简单多了,只需要知道客户业务的资源,然后写写配置文件,最后执行了一个命令来生成应用程序就好了。不过如果你和我一样生活在现实世界的话,还是趁早放弃这种完全自动化的想法吧。

复杂的业务

现实世界的软件开发是复杂的,复杂性并不体现在具体的技术栈上。如Java,Spring,Docker,MySQL等等具体的技术是可以学习很快就熟练掌握的。软件真正复杂的部分,往往是业务本身,比如航空公司的超售策略,在超售之后Remove乘客的策略等;比如亚马逊的打折策略,物流策略等。

用软件模型如何优雅而合理的反应复杂的业务(以便在未来业务发生变化时可以更快速,更低错误的作出响应)本身也是复杂的。要将复杂的业务规则转换成软件模型是软件活动中非常重要的一环,也是信息传递往往会失真的一环。业务人员说的A可能被软件开发者理解成Z,反过来也一样。

举个例子,我给租来的房子买了1年的联通宽带。可是过了6个月后,房东想要卖房子把我赶了出来,在搬家之后,我需要通知联通公司帮我做移机服务。

如果纯粹从开发者的角度出发,写出来的代码可能看起来是这样的:

public class Customer {
     private String address;

     public void setAddress(String address) {
         this.address = address;
     }

     public String getAddress() {
         return this.address;
     }
}

中规中矩,一个简单的值对象。作为对比,通过与领域专家的交流之后,写出来的代码会是这样:

public class Customer {
     private String address;

         public void movingHome(String address) {
         this.address = address;
     }
}

通过引入业务场景中的概念movingHome,代码就变得有了业务含义,除了可读性变强之外,这样的代码也便于和领域专家进行交流和讨论。Eric在领域驱动设计(Domain Drvien Design)中将统一语言视为实施DDD的先决条件。

层次架构(三明治)

All problems in computer science can be solved by another level of indirection, except of course for the problem of too many indirections. — David Wheeler

上文提到,业务系统对外的呈现是对某种资源的管理,而且,现实世界里的业务系统往往要对多种资源进行管理。这些资源还会互相引用,互相交织。比如一个看板系统中的泳道、价值流、卡片等;LinkedIn中的公司,学校,个人,研究机构,项目,项目成员等,它们往往会有嵌套、依赖等关系。

为了管理庞大的资源种类和繁复的引用关系,人们自然而然的将做同样事情的代码放在了统一的地方。将不同职责的事物分类是人类在处理复杂问题时自然使用的一种方式,将复杂的、庞大的问题分解、降级成可以解决的问题,然后分而治之。

(图片来自:http://t.cn/RSNienv)

比如在实践中 ,展现部分的代码只负责将数据渲染出来,应用部分的代码只负责序列化/反序列化、组织并协调对业务服务的调用,数据访问层则负责屏蔽底层关系型数据库的差异,为上层提供数据。这就是层级架构的由来:上层的代码直接依赖于临近的下层,一般不对间接的下层产生依赖,层次之间通过精心设计的API来通信(依赖通常也是单向的)。

以现代的眼光来看,层次架构的出现似乎理所应当、自然而然,其实它也是经过了很多次的演进而来的。以JavaEE世界为例,早期人们会把应用程序中负责请求处理、文件IO、业务逻辑、结果生成都放在servlet中;后来发明了可以被Web容器翻译成servlet的JSP,这样数据和展现可以得到比较好的分离(当然中间还有一些迂回,比如JSTL、taglib的滥用又导致很多逻辑被泄露到了展现层);数据存储则从JDBC演化到了各种ORM框架,最后再到JPA的大一统。

如果现在把一个Spring-Boot写的RESTful后端,和SSH(Spring-Struts-Hibernate)流行的年代的后端来做对比,除了代码量上会少很多以外,层次结构上基本上并无太大区别。不过当年在SSH中复杂的配置,比如大量的XML变成了代码中的注解,容器被内置到应用中,一些配置演变成了惯例,大致来看,应用的层次基本还是保留了:

  • 展现层
  • 应用层
  • 数据访问层

在有些场景下,应用层内还可能划分出一个服务层。

前后端分离

随着智能设备的大爆发,移动端变成了展现层的主力,如何让应用程序很容易的适配新的展现层变成了新的挑战。这个新的挑战驱动出了前后端分离方式,即后端只提供数据(JSON或者XML),前端应用来展现这些数据。甚至很多时候,前端会成为一个独立的应用程序,有自己的MVC/MVP,只需要有一个HTTP后端就可以独立工作。

前后端分离可以很好的解决多端消费者的问题,后端应用现在不区分前端的消费者到底是谁,它既可以是通过4G网络连接的iOS上的Native App,也可以是iMac桌面上的Chrome浏览器,还可以是Android上的猎豹浏览器。甚至它还可以是另一个后台的应用程序:总之,只要可以消费HTTP协议的文本就可以了!

这不得不说是一个非常大的进步,一旦后端应用基本稳定,频繁改变的用户界面不会影响后端的发布计划,手机用户的体验改进也与后端的API设计没有任何关系,似乎一切都变的美好起来了。

业务与基础设施分离

不过,如果有一个消费者(一个业务系统),它根本不使用HTTP协议怎么办?比如使用消息队列,或者自定义的Socket协议来进行通信,应用程序如何处理这种场景? 这种情况就好比你看到了这样一个函数:

httpService(request, response);

作为程序员,自然会做一次抽象,将协议作为参数传入:

service(request, response, protocol);

更进一步,protocol可以在service之外构造,并注入到应用中,这样代码就可以适配很多种协议(比如消息队列,或者其他自定义的Socket协议)。 比如:

public interface Protocol {
  void transform(Request request, Response response);
}

public class HTTP implements Protocol {
}

public class MyProtocol implements Protocol {
}

public class Service {     
   public Service(Protocol protocol) {
         this.protocol = protocol;     
   }          

   public void service(request, response) {
         //business logic here
         protocol.transfrom(request, response);     
   }
}

类似的,对于数据的持久化,也可以使用同样的原则。对于代码中诸如这样的代码:

persisteToDatabase(data);

在修改之后会变成: persistenceTo(data, repository);

应用依赖倒置原则,我们会写出这样的形式:

public class DomainService {
     public BusinessLogic(Repository repository) {
           this.repository = repository
     }

      public void perform() {
           //perform business logic
           repository.save(record);
     }
}

对于Repository可能会有多种实现。根据不同的需求,我们可以自由的在各种实现中切换:

public class InMemoryRepository implements Repository {}
public class RDBMSRepository implements Repository {}

这样业务逻辑和外围的传输协议、持久化机制、安全、审计等等都隔离开来了,应用程序不再依赖具体的传输细节,持久化细节,这些具体的实现细节反过来会依赖于应用程序。

通过将传统内置在层次架构中的数据库访问层、通信机制等部分的剥离,应用程序可以简单的分为内部和外部两大部分。内部是业务的核心,也就是DDD(Domain Driven Design)中强调的领域模型(其中包含领域服务,对业务概念的建立的模型等);外部则是类似RESTful API,SOAP,AMQP,或者数据库,内存,文件系统,以及自动化测试。

这种架构风格被称为六边形架构,也叫端口适配器架构。

六边形架构(端口适配器)

六边形架构最早由Alistair Cockburn提出。在DDD社区得到了发展和推广,然后IDDD(《实现领域驱动设计》)一书中,作者进行了比较深入的讨论。

(图片来自:slideshare.net )

简而言之,在六边形架构风格中,应用程序的内部(中间的橙色六边形)包含业务规则,基于业务规则的计算,领域对象,领域事件等。这部分是企业应用的核心:比如在线商店里什么样的商品可以打折,对那种类型的用户进行80%的折扣;取消一个正在执行的流水线会需要发生什么动作,删除一个已经被别的Job依赖的Stage又应该如何处理。

而外部的,也是我们平时最熟悉的诸如REST,SOAP,NoSQL,SQL,Message Queue等,都通过一个端口接入,然后在内外之间有一个适配器组成的层,它负责将不同端口来的数据进行转换,翻译成领域内部可以识别的概念(领域对象,领域事件等)。

内部不关心数据从何而来,不关心数据如何存储,不关心输出时JSON还是XML,事实上它对调用者一无所知,它可以处理的数据已经是经过适配器转换过的领域对象了。

六边形架构的优点

  • 业务领域的边界更加清晰
  • 更好的可扩展性
  • 对测试的友好支持
  • 更容易实施DDD

要新添加一种数据库的支持,或者需要将RESTful的应用扩展为支持SOAP,我们只需要定义一组端口-适配器即可,对于业务逻辑部分无需触碰,而且对既有的端口-适配器也不会有影响。

由于业务之外的一切都属于外围,所以应用程序是真的跑在了Web容器中还是一个Java进程中其实是无所谓的,这时候自动化测试会容易很多,因为测试的重点:业务逻辑和复杂的计算都是简单对象,也无需容器,数据库之类的环境问题,单元级别的测试就可以覆盖大部分的业务场景。

这种架构模式甚至可能影响到团队的组成,对业务有深入理解的业务专家和技术专家一起来完成核心业务领域的建模及编码,而外围的则可以交给新人或者干脆外包出去。

在很多情况下,从开发者的角度进行的假设都会在事后被证明是错误的。人们在预测软件未来演进方向时往往会做很多错误的决定。比如对关系型数据库的选用,对前端框架的选用,对中间件的选用等等,六边形架构可以很好的帮助我们避免这一点。

小结

软件的核心复杂度在于业务本身,我们需要对业务本身非常熟悉才可能正确的为业务建模。通过统一的语言我们可以编写出表意而且易于和业务人员交流的模型。

另一方面模型应该尽可能的和基础设施(比如JSON/XML的,数据库存储,通信机制等)分离开。这样一来可以很容易用mock的方式来解耦模型和基础设施,从而更容易测试和修改,二来我们的领域模型也更独立,更精简,在适应新的需求时修改也会更容易。

这里有一段很微小的代码,有兴趣的同学可以看看。


更多精彩洞见,请关注微信公众号:思特沃克

Share

一张漂亮的可视化图表背后

可视化之根

多年前读过一篇非常震撼的文章,叫《Lisp之根》(英文版:The roots of Lisp),大意是Lisp仅仅通过一种数据结构(列表)和有限的几个函数,就构建出了一门极为简洁,且极具扩展性的编程语言。当时就深深的被这种设计哲学所震撼:一方面它足够简单,每个单独的函数都足够简单,另一方面它有非常复杂,像宏,高阶函数,递归等机制可以构建出任意复杂的程序,而复杂的机制又是由简单的组件组成的。

数据的可视化也是一样,组成一幅内容清晰、表达力强、美观的可视化信息图的也仅仅是一些基本的元素,这些元素的不同组合却可以产生出令人着迷的力量。

要列出“可视化元素之根”很容易:位置、长度、角度、形状、纹理、面积(体积)、色相、饱和度等几种有限的元素,邱南森在他的《数据之美》中提供了一张视觉元素的图,其中包含了大部分常用的元素。

令人振奋的是,这些元素可以自由组合,而且组合往往会产生1+1>2的效果。

心理学与认知系统

数据可视化其实是基于人类的视觉认知系统的,因此对人类视觉系统的工作方式有一些了解可以帮助我们设计出更为高效(更快的传递我们想要表达的信息给读者)的可视化作品。

心理物理学

在生活中,我们会遇到这样的场景:一件原价10元的商品,如果降价为5元,则消费者很容易购买;而一件原价100元的商品,降价为95元,则难以刺激消费者产生购买的冲动。这两个打折的绝对数字都是5元,但是效果是不一样的。

韦伯-费希纳定理描述的正是这种非理性的场景。这个定理的一个比较装逼的描述是:

感觉量与物理量的对数值成正比,也就是说,感觉量的增加落后于物理量的增加,物理量成几何级数增长,而心理量成算术级数增长,这个经验公式被称为费希纳定律或韦伯-费希纳定律。

– 摘自百度百科

这个现象由人类的大脑构造而固有,因此在设计可视化作品时也应该充分考虑,比如:

  • 避免使用面积图作为对比
  • 在做对比类图形时,当差异不明显时需要考虑采用非线性的视觉元素
  • 选用多种颜色作为视觉编码时,差异应该足够大

比如:

如上图中,当面积增大之后,肉眼越来越难从形状的大小中解码出实际的数据差异,上边的三组矩形(每行的两个为一组),背后对应的数据如下,可以看到每组中的两个矩形的绝对差都是5:

var data = [
  {width: 5, height: 5},
  {width: 10, height: 10},

  {width: 50, height: 50},
  {width: 55, height: 55},

  {width: 100, height: 100},
  {width: 105, height: 105}
];

格式塔学派

格式塔学派是心理学中的一个重要流派,她强调整体认识,而不是结构主义的组成说。格式塔认为,人类在看到画面时,会优先将其简化为一个整体,然后再细化到每个部分;而不是先识别出各个部分,再拼接为整体。

比如那条著名的斑点狗:

我们的眼睛-大脑可以很容易的看出阴影中的斑点狗,而不是先识别出狗的四条腿或者尾巴(事实上在这张图中,人眼无法识别出各个独立的部分)。

格式塔理论有几个很重要的原理:

  • 接近性原理
  • 相似性原理
  • 封闭性原理
  • 连续性原理
  • 主体/背景原理

当然,格式塔学派后续还有一些发展,总结出了更多的原理。工程上,这些原理还在大量使用,指导设计师设计各式各样的用户界面。鉴于网上已经有众多的格式塔理论及其应用的文章,这里就不在赘述。有兴趣的同学可以参考这几篇文章:

视觉设计的基本原则

《写给大家看的设计书》一书中,作者用通俗易懂的方式给出了几条设计的基本原则,这些原则完全可以直接用在数据可视化中的设计中:

  • 亲密性(将有关联的信息物理上放在一起,而关联不大的则通过留白等手段分开)
  • 对齐(将元素通过水平,垂直方向对齐,方便视觉识别)
  • 重复(重复使用某一模式,比如标题1的字体颜色,标题2的字体颜色等,保持重复且一致)
  • 对比(通过强烈的对比将不同的信息区分开)

如果稍加留意,就会发现现实世界中在大量的使用这几个原则。1,2,3三个标题的形式就是重复性的体现;每个标题的内容自成一体是因为组成它的元素(数字,两行文字)的距离比较近,根据亲密性原则,人眼会自动将其归为一类;超大的数字字体和较小的文字形成了对比;大标题的颜色和其他内容形成了对比等等。

这些原则其实跟上面提到的格式塔学派,以及韦伯-费希纳定理事实上是相关的,在理解了这些人类视觉识别的机制之后,使用这些原则就非常自然和得心应手了。

一些例子

  • 淡化图表的网格(和数据图形产生对比)
  • 通过深色来强调标尺(强烈的线条和其余部分产生对比)
  • 离群点的高亮(通过不同颜色产生对比)
  • 使用颜色(通过不同的颜色,利用亲密性原则方便读者对数据分组)
  • 元素颜色和legend(使用重复性原则)
  • 同一个页面上有多个图表,采取同样的图例,色彩选择(强调重复性原则)

实例

上篇文章提到我通过一个手机App收集到了女儿成长的一些记录,包括哺乳信息,换尿布记录,以及睡眠信息。这个例子中,我会一步步的介绍如何将这些信息可视化出来,并解释其中使用的视觉原理。

可视化的第一步是要明确你想要从数据中获取什么信息,我想要获取的信息是孩子的睡眠总量以及睡眠时间分布情况。

进阶版的条形图

确定了可视化的目的之后,第二步是选取合适的视觉编码。上面提到过,对于人眼来说,最精确的视觉编码方式是长度。我们可以将睡眠时间转化为长度来展现,最简单的方式是按天聚合,然后化成柱状图。比如:

2016/11/21,768
2016/11/22,760
2016/11/23,700

不过这种图无法看出时间的分布。我们可以考虑通过条形图的变体来满足前面提到的两个核心诉求。先来在纸上画一个简单的草图。纵轴是24小时,横轴是日期。和普通的条形图不一样的是,每个条形的总长度是固定的,而且条形代表的不是简单非数据类型,而是24小时。在草稿中,每个画斜线的方块表示孩子在睡眠状态,而虚线部分表示她醒着。

原始数据

name,date,length,note
心心,2016/11/21 19:23,119,
心心,2016/11/21 22:04,211,
心心,2016/11/22 02:03,19,
心心,2016/11/22 02:23,118,
心心,2016/11/22 05:58,242,
心心,2016/11/22 10:57,128,
心心,2016/11/22 14:35,127,
心心,2016/11/22 17:15,127,
心心,2016/11/22 20:02,177,
心心,2016/11/23 01:27,197,

这里有个问题,我们的纵轴是24小时,如果她晚上23点开始睡觉,睡了3个小时,那么这个条形就回超出24格的轴。我写了一个函数来做数据转换:

require 'csv'
require 'active_support/all'
require 'json'

csv = CSV.read('./visualization/data/sleeping_data_refined.csv', :headers => :first_row)

data = []
csv.each do |row|
    date = DateTime.parse(row['date'], "%Y/%m/%d %H:%M")

    mins_until_end_of_day = date.seconds_until_end_of_day / 60
    diff = mins_until_end_of_day - row['length'].to_i

    if (diff >= 0) then
        data << {
            :name => row['name'],
            :date => row['date'],
            :length => row['length'],
            :note => row['note']
        }
    else
        data << {
            :name => row['name'],
            :date => date.strftime("%Y/%m/%d %H:%M"),
            :length => mins_until_end_of_day,
            :note => row['note']
        }

        data << {
            :name => row['name'],
            :date => (date.beginning_of_day + 1.day).strftime("%Y/%m/%d %H:%M"),
            :length => diff.abs,
            :note => row['note']
        }
    end
end

有了干净的数据之后,我们可以编写一些前端的代码来绘制条形图了。画图的时候有几个要注意的点:

  • 每天内的时间段对应的矩形需要有相同的X坐标
  • 不同的睡眠长度要有颜色区分(睡眠时间越长,颜色越深)
var dateRange = _.uniq(data, function(d) {
  var date = d.date;
  return [date.getYear(), date.getMonth(), date.getDate()].join("/");
});

xScale.domain(dateRange.map(function(d) { return d.date; }));

function getFirstInDomain(date) {
  var domain = xScale.domain();

  var index = _.findIndex(domain, function(d) {
      return date.getYear() === d.getYear()
          && date.getMonth() === d.getMonth()
          && date.getDate() === d.getDate();
  });

  return domain[index];
}

函数getFirstInDomain可以根据一个日期值返回一个X坐标,这样2016/11/21 19:232016/11/21 22:04都会返回一个整数值(借助d3提供的标尺函数)。

另外,我们根据每次睡觉的分钟数将睡眠质量划分为5个等级:

var level = d3.scale.threshold()
  .domain([60, 120, 180, 240, 300])
  .range(["low", "fine", "medium", "good", "great", "prefect"]);

然后在绘制过程中,根据实际数据值来确定不同的CSS Class

svg.selectAll(".bar")
  .data(data)
  .enter()
  .append("rect")
  .attr("class", function(d) {
      return level(d.length)+" bar";
  })
//...

实现之后,看起来是这个样子的。事实上这个图标可以比较清楚的看出大部分睡眠集中在0-6点,而中午的10-13点以及黄昏18-20点基本上只有一些零星的睡眠。

星空图

上面的图有一个缺点,是当日期很多的时候(上图差不多有100天的数据),X轴会比较难画,如果缩减成按周,或者按月,又会增加很多额外的复杂度。

另外一个尝试是变形:既然这个统计是和时间相关的,那么圆形的钟表形象是一个很好的隐喻,每天24小时自然的可以映射为一个圆。而睡眠时间可以通过弧长来表示,睡眠时间越长,弧长越大:

角度转弧度

我们首先将整个圆(360度)按照分钟划分,则每分钟对应的角度数为:360/(24*60),再将角度转化为弧度:degree * π/180

var perAngle = (360 / (24 * 60)) * (Math.PI/180);

那么对于指定的时间,比如10:20,先计算出其分钟数:10*60+20,再乘以preAngle,就可以得出起始弧度;起始时间的分钟数加上睡眠时长,再乘以preAngle,就是结束弧度。

function startAngle(date) {
    var start = (date.getHours() * 60 + date.getMinutes()) * perAngle;
    return Math.floor(start*1000)/1000;
}

function endAngle(date, length) {
    var end = (date.getHours() * 60 + date.getMinutes() + length) * perAngle;
    return Math.floor(end*1000)/1000;
}

实现的结果是这样的:

初看起来,它像是星空图,但是图中的不同颜色含义没有那么直观,我们需要在图上补充一个图例。通过使用d3的线性标尺和定义svg的渐变来实现,定义好渐变和渐变的颜色取值范围之后,就可以来绘制图例了。

图上的每段弧都会有鼠标移动上去的tooltip,这样可以很好的和读者大脑中的钟表隐喻对照起来,使得图表更容易理解。

由于我将整个圆分成了24份,这点和普通的钟表事实上有差异,那么如果加上钟表的刻度,会不会更好一些呢?从结果来看,这样的标线反而有点画蛇添足,所以我在最后的版本中去掉了钟表的标线。

可以看到,我们通过圆形的钟表隐喻来体现每一天的睡眠分布,然后用颜色的深浅来表示每次睡眠的时长。由于钟表的形象已经深入人心,因此读者很容易发现0点在圆环群的正上方。中心的黄色实心圆帮助读者视线先聚焦在最内侧的圆上,然后逐渐向外,这和日期的分布方向正好一致。

最终的结果在这里:心心的睡眠记录,完整的代码在这里

更进一步

一个完整的可视化作品,不但要运用各种视觉编码来将数据转换为视觉元素,背景信息也同样重要。既然这个星空图是关于睡眠主题的,那么一个包含她在睡觉的图片集合则会加强这种视觉暗示,帮助读者快速理解。

制作背景图

我从相册中选取了很多女儿睡觉时拍的照片,现在需要有个工具将这些照片缩小成合适大小,然后拼接成一个大的图片。这其中有很多有趣的地方,比如图片有横屏、竖屏之分,有的还是正方形的,我需要让缩放的结果是正方形的,这样容易拼接一些。

好在有imagemagick这种神器,只需要一条命令就可以做到:

$ montage *.jpg -geometry +0+0 -resize 128x128^ \
-gravity center -crop 128x128+0+0 xinxin-sleeping.jpg

这条命令将当前目录下的所有的jpg文件缩放成128×128像素,并从中间开始裁剪-gravity center+0+0表示图片之间的缝隙,最后将结果写入到xinxin-sleeping.jpg中。

拼接好图片之后,就可以通过CSS或者图片编辑器为其添加模糊效果,并设置深灰色半透明遮罩。

body {
  background-image:url('/xinxin-sleeping.png');
  background-size:cover;
  background-position:center;
}

当然,背景信息只是补充作用,需要避免喧宾夺主。因此图片做了模糊处理,且加上了深灰色的半透明Mask(此处应用了格式塔理论中的主体/背景原理)。

小结

这篇文章讨论了可视化作品背后的一些视觉元素理论,以及人类的视觉识别机制。在这些机制的基础上,介绍了如何运用常用的设计原则来进行视觉编码。最后,通过一个实例来介绍如何运用这些元素 – 以及更重要的,这些元素的组合 – 来制作一个漂亮的、有意义的可视化图表。

参考资料

这里有一些关于认知系统和设计原则的书籍,如果你感兴趣的话,可以用来参考


更多精彩洞见,请关注微信公众号:思特沃克

Share