开发团队的自我救赎 (重发)

前言

前段时间(2018年了) 受邀到厦门线下分享,后来也在众学院上在线分享这个主题。自我回顾,在有限的时间内,确实很难把整个主题全部详细分享。Eric建议我把它再次整理成文章,方便更多的伙伴借鉴。

曾经我们非常差,现在的我们做的并没有多牛X,只是比以前要好一些,并在持续改进。

注: 文中图片均收集自网络,非常贴切表达我们的情况,如有侵权,请联系我删除图片。

追求技术卓越 - 开发团队的自我救赎

为什么会分享这个主题?

因为经历了太多相似的 失败
工作这么多年,成功的经历较少,失败的经历众多。IT产品/项目大多数的失败,背后都有类同的共性,大体可以归为两大类问题: 产品规划问题技术实践问题
这里先聚焦在 技术实践问题

产品/项目的共性

  • 工期紧
  • 任务重
  • 难度大
  • 人手少

通常便带来这些场景

  • 明确需求,验收标准
    时间紧,边做边整吧。

  • TDD、单元测试
    做不到,不现实。

  • CI/CD
    不可能。单元测试时CI的前提,没有单元测试,对于编译类语言(如 java),顶多称为每日构建;假如是解释型语言(javascript, php, python, ruby), 连每日构建都算不上。

  • 质量保证
    人肉。 由于没有单元测试,所有的测试都只能人肉完成。质量与成本与进度便是一组矛盾。

  • 架构设计
    来不及了,啥也别说,赶紧开车吧

如果你的项目情况有上面三点以上相似的问题,那么下面的场景,你应当会感到亲切与熟悉 😉

项目开始

项目开始

对于这类项目,没有有效的工作边界(明确的完工标准),缺乏制动化质量保证,也无从说起有效的持续集成、持续发布,我们称为 裸奔型项目

项目进展

裸奔的进度

正因为 裸奔, 抛弃了许多技术实践的工作,因此,我们在初期阶段便获得了 “飞一般的” 的开发进度。某个户外APP项目,我们3个月 9-9-6 干出等同于 正常情况(有单元测试,有CI/CD ...) 6个月的功能数量。 与此同时,我们累积的大量的技术债务。

进度泥潭

我们的进度开始被大量的缺陷拖住,每天在改BUG,每天在加班制作更多的BUG。到后来,修改代码都变得小心翼翼的。

生怕改一行代码,怼垮一片功能。

牵一发、动全身

解决了一个问题,发现还有更多的问题在背后。

一个问题的背后是更多问题

所有的环境全靠手工部署,各种对接问题,项目交付演示状况频发。

阶段交付演示

测试环境一切OK了,好不容易熬到上线

上线既炸裂

三个月赶完上线的APP,后期花了六个月的时间修修补补。

团队的状态

运维全靠人肉,每次假期,都得先求不崩。我自己有一次休长假,都得随身带着笔记本处理突发状况。

放假之前

新的项目开始

新的轮回开始~


.

我们在努力思考,如何才能改变这种状况?问题出在哪里?

是我们不够努力?

  • 实际上 996 的加班方式已是常态,升级到007就能改善吗?

    答案是NO,长期的加班,并没有带来有效产出的增长,然而收获更多无效的产出以及更低的质量。对团队的成长也是一种伤害。
    长期加班,多数可以归纳为

    1. 通过加班来掩盖能力的不足,从而保护自己免受进度、质量、能力的质疑;
    2. 通过加班来营造勤奋的表现,反而能够获得管理层的好评,加薪,升职。
    3. 团队领导者的不足;
    4. 产品战略与规划的不足;

    所以,我们反思
    加班有没有帮助公司创造真正的价值?如果没有,便是无效的加班。
    我们到底是在加班产出功能,还是加班产出BUG? BUG是有成本代价的

    我们反对把加班当成银弹:没有加班解决不了的,如果有,那就加更多的班。
    我们并不是完全反对加班,追赶关键节点是,该加班就加班,要有充分的价值。

    当然,有些公司把加班视为政治正确,对于这种公司,无法评价。

是软件管理过程不够好?

  • PMI、CMMI、"敏捷" 我们都实践过
  • NO! 每一种过程实际上都很好,问题的关键,是我们的能力与过程是否匹配?

用敏捷,不一定就能成功;用传统方式,不一定就会失败;

当 "我" 用PMI的项目管理方式,失败了,并不能说明PMI的项目管理方式不行,更多说明的是 "我" 的能力需要提升。


我们反思现状,导致我们(公司与团队)陷入这种泥潭的多种原因。

根源之一便是

公司愿景目标(营收期望) 与 团队的实际能力(现实)的矛盾

期望与现实的差距 = 压力与焦虑
差距越大,焦虑与压力变越大。但是,焦虑与压力本身,并不能改变现状和解决问题。

正向的认知,应当是

  • 营收的创造与提升,来自于团队向外产出的价值。

  • 团队产出价值的提升 => 营收的提升。


如何提升团队产出价值?

image.png

用我们想开一辆车从 A地 出发去 B地,来类比项目的情况。
想尽快(更少的日历时间)到达目的地,有几种常见的方式:

  1. 每天多开几个小时 ----- 加班.
  2. 替换更高效的变速箱,提升对发动机动力的转换效率,减少损耗,达到动力不变的情况下,有效提升速度 ----- 改进工作过程.
  3. 替换发动机,更大的动能输出,提升速度 ----- 提升团队能力.
  • 大部分公司和团队,会首选第一种方式 - 加班, 因为 简单直接粗暴有效, 还有最具有吸引力的一个特点 可操作性高。还能营造出 大干快干100天 的"积极奋斗"形象。

  • 有一些公司的格局更高一些,思考用采用改进工作过程来提升效率。改进工作过程,能够有效的消除过程的等待与浪费,从而提升效率。通常能很快看到一些收效,也具有极高的可操作性。但是不能直接改变能力现状。

  • 少数公司会考虑通过提升团队的能力和认知,来直接提升效率。大家都认可团队能力的提升,能直接提升有效交付。但是这种方式的操作难度是最大的:

    1. 能力的提升是一个长期且持续的过程,不是一个短期的一次性投入行为。
    2. 如何才能提升团队的能力?
    3. 把人培养出来后,Ta会不会跑?

    问自己一个问题, 如果团队的能力没有发生任何变化,我们的能否获得更多更好的交付?如果更多更好的交付,来自于团队能力的提升,那么便应当努力帮助管对提升能力,在这个过程中,公司已经在持续享用这个收益。
    在一个加薪周期内,员工因能力提升而产出更多的价值和贡献,都是公司的增值收益, 也是鼓励公司努力去获取的;到了加薪周期,公司也应当正向对员工的贡献给出反馈,并继续提供条件与鼓励员工持续提升,Win-Win, 是否需要担心Ta是否会跑?优秀的公司与团队,会吸引更多优秀的人。


调整认知

基于前面的思考,我们尝试调整认知:

  • 任何没有实际改进能力的过程,最终效果甚微。

    觉得现有的过程,效果一般般;换一个过程,团队需要去适应,能力却没有变化,折腾一段时间,如果没有明显效果变化;最佳反应: 这个过程不太适合我们,再换另一个过程试试,一切是过程的锅。像不像在换另一种方式耍猴

  • 凡事不要急于下结论“不可能、做不到、不现实”

    现在做不到、不现实的事情,是因为我们现在的认知、知识、经验没有覆盖到,并不代表没有人能做到。
    转而探寻: “我如何才能做到”,“我该学习什么”,“我该改变什么认知”,“谁可以帮助我”。

    现在做不到的,就去学习,去探索,去提升,去思考如何能做到。

  • 系统是资产,代码是负债。

    在运行的系统,是公司的资产,产生收益。背后的代码,却是负债,代码量越大,负债越多。

    举一个例子,在一次线下分享中,说到重构,有个小伙伴就说,他们的系统代码量已达25万行之多,几乎没人敢做任何重构的动作,只能不断的边累加代码边忍受。

    另外一种情况,如果一个功能,只有百来十行代码,甚至两百来行,如果有问题,你可能毫不犹豫的就重写了。

    因此,尽可能的让功能/模块的代码简洁短小,且保存独立,对外部来说,最好是成为一个黑箱子。重构甚至重写的代价与风险便最小化。

  • 质量是靠开发团队创造出来的,而不是测试人员测试出来的

    BUG是代码的问题,代码是开发人员写的,质量的首要负责人便是开发人员,而不是测试人员。

    见过不少开发人员,代码一写完,自己都没怎么测,就提交,让后便叫某某某,帮我测试这个功能。 WTF~ ,团队中测试人员占比越多的,越是这种情况,交付质量越是糟糕。测试人员被开发人员用来擦屁股。

    我后来引导的团队,没有测试人员,交付质量反而更高。


改变的动力

调整了认知后,支持我们寻找改变的动力

  • $$$ - 收入是第一原动力

    • 用更少的代码,实现更多的价值

      寻找表达力更佳的语言和工具,提升效率

    • 在相同规模的时间,产出更多的价值(营收、工资)

      如何在相同的时间,获得更多的收益

    • 在相同规模的价值,使用更少的时间完成

      如果收入无法提升,那么如何用更少的时间工作,从而有更多的时间去学习和提升自己

  • 工作与生活平衡,工作是为了更美好生活

    工作的目的不是为了加班,而是为了给自己与家人更美好的生活。


达成一致的认知

通过讨论沟通,我们达成需要遵守的一致认知

  • 遵从工程师文化

    • 永远尝试通过技术的手段来解决管理上的问题
      把管理的问题,转化为技术的问题。如质量保证,通过自动化测试 CI来实现;代码评审问题,通过代码检测工具实现基本的自动化评审...

      一个团队/公司管理的比重越大,说明需要改进与提升的空间越大。

    • 谁牛(技术,思想,方法等)听谁
      跟比自己厉害的伙伴学习,尝试、实践、反馈。让自己厉害起来。

    • 自己的狗粮自己吃
      质量的问题,谁开发谁保障。
      You code it,
      You test it,
      You build it,
      You run it.

    • 保持对技术的好奇心,追求卓越

  • 相比追求进度,我们更关注质量与技术债务

    进度很重要,但并不意味着,可以为了进度而牺牲质量,出来混终归是要还的。这个问题在另外几篇文章已经多次提及,这里就不再重复。我们想要获得的是可持续提升的进度。

    做得快并不代表做得好。

  • 通过能力改变提升交付,而非加班提升交付

    加班是可以短期提升产能,长期加班产能与质量反而下降,这点大家心知肚明。所以我们希望正向的循环:在时间不变的情况下,能力提升 获得 交付提升, 交付提升 获得 价值提升。

  • 强制 不加班,不加班,不加班

    当时团队强制不加班的约定,上班的时间全力专注于交付,下班就走人。工作中遇到需要学习的、探索的部分和遇到的难题,下班把问题带回去,自己安排时间思考和学习,上班时把解决方案带回来。

    原因很简单,上班的时间全力在做交付产出时,人的思维与精神通常有些疲惫了,延长时间效率下降,甚至容易钻牛角尖;有次遇到一个问题,加班到晚上11点多,终于解决了。回家冲澡过程中,灵光一现,想到另一种解决方式,只需不到半小时就能搞定,效果还更好。 5个来小时钻牛角尖的成果,还不如半小时的新思路,张弛有度很重要。


解决方案

如何才能把前面刷新的认知,变成现实?如何把握好经济实用技术追求之间的平衡?

如果过于侧重经济性(eg. 低水平开发人员),追求进度,我们往往得到满足于当前需求的脆弱而苟且的架构与代码,扩展与维护的代价随着时间指数增长。

如果过于侧重技术追求(eg. 高水平开发人员),首先高水平人员相对稀缺,其次成本通常也比较高昂。好处是扩展与维护的代价是线性的。

我不断学习探索各种技术,主要的目的也就是既想经济适用的同时技术上也不苟。

所以通过 合适的语言、合适的过程、合适的工具 可以帮助我们做好这些平衡。

  • ❌ JAVA, PHP

JAVA PHP 是当前占有量最大的,市场职位数量与劳动力数量也是最大的。许多公司选择JAVA PHP,一个重要的考虑就是,市场有大量的人员补充,走了谁都不怕,两条腿的码农到处都是。大多数人员选择这两种语言,是因为好找工作。

基数大了,十几K的代码搬运工程师的比例也就大了,感受如何,合作过的就知道。

优秀的JAVA开发人员不少,但其成本并非大部分初创公司、小型公司所能承担。一名熟练TDD的JAVA开发人员,成本不低于30K+。

JAVA语言,把一个初级开发人员培养到能TDD的水平,需要的时间相对比较长。

我们没有考虑JAVA的另一个主要原因就是 穷 没钱
不缺钱的公司,尽量考虑JAVA吧。

  • ✅ Ruby (RubyOnRails), Elixir

    既要控制成本,又想要获得更好的质量与交付能力,我们便选了 RubyOnRails (后来技术栈又往 Elixir 迁移, 后期有空再聊聊这个话题)。原因是:

    • 超强表达力,相同的功能,代码量仅为 JAVA PHP 40%左右
    • 开发效率更高
    • 社区虽然小,但却更有技术追求
    • 因为小众,天生自带猪队友过滤效果
    • 语言与框架对测试(单元测试、功能测试、集成测试、验收测试)的支持度非常高
    • 五六个月左右的时间,我们能把RubyOnRails开发人员培养到具备TDD的能力
      (注: 后来在另一个团队,使用 Elixir, 这个时间缩短到三个月左右 😏)

    天下没有免费的午餐, 有明显的好处,也就有明显的弱处:

    • 小众,开发人员不好找,通常只能自己培养。
    • 现有的团队未必个个都愿意接受。曾经当我们表达想从PHP转Ruby,将近三分之一的PHP开发人员明确表示,用Ruby他们就离职,因为PHP好找工作, 换了Ruby后不好找。 😂
    • 需要公司有想成为一家卓越的公司的野望。意味着要重视人才,公司的成长与核心竞争力,来自于公司拥有多少有追求的人才。不是一味追求最低成本。

    选用Ruby / Elixir,人均成本不是更低,反而会比普通的java php等高一些。
    但是,总成本却会更低,原因: 正常情况下,3个Ruby/Elixir的开发人员,有效产能(进度与质量) >= 5~6个 java/php人员。而且还能保持非常低的技术债务,后续的进度与质量不会下降,反而能持续提升。

    在同等质量和同等时间的条件下,达到3个人员拿原来4个人的薪水,做出原来5~6个人的工作。


认知改变了,方案有了,那就执行吧,由于一开始团队就很弱鸡,整个成长过程,分了几个阶段:

能力提升第一阶段

  • 提升目标

    • 遵从统一的技术实践,代码风格
    • 做到小步频繁提交代码
    • 通过部署脚本实现一条命令完成部署升级
  • 成果

    • 改进工作习惯
    • 提升代码与设计能力
    • 获得配置友好的系统架构,开发到部署变得平滑

整个阶段大约用了一个多两个月的时间, 扭转了不少不良习惯,提升了团队的开发能力。所有人从在Windows上开发,全部切换到Ubuntu上开发,迫使开发人员自己写的功能,是如何在服务器上运行和部署、以及如何排错问题。切换的第一周,开发人员各自不适用和痛苦,一周后,习惯了就好 😜 。

关于开发人员的操作系统,有兴趣可以浏览另一篇文章 为什么我推荐程序员的标配是Mac (i7 16G 双显)


能力提升第二阶段

  • 提升目标

    • 实践DDT (Development Driven Test), 先写实现代码再写单元测试
    • 通过 Pull Request 方式提交代码合并请求,交叉评审
    • 实现CI (持续集成)
    • 集成性能探针,错误报告等工具
  • 成果

    • 通过单元测试,迫使代码结构的优化与改进
    • 同一段代码至少有两个以上的伙伴了解,知识也得到传递
    • 通过CI质量开始得到保障
    • 整个系统的负载、错误,透明化可视化,为系统持续改进提供数据
这个阶段,是最困难的一个阶段。我们要解决一个大多数公司都共同的问题: 要TDD,团队得有TDD的能力。鸡生蛋问题。

我们的现状是开发人员只是听过单元测试,没有实际写过一个单元测试,更无从TDD了。但是我们现在做不到,并不代表未来也做不到。

image.png

现在做不到TDD,就先做到DDT,先写业务代码,在编写对应的单元测试代码;确保每一个API,都有充足的测试用例。

在这个痛苦的过程中,让团队体会到 "可工作代码" 与 "可以测试代码" 是两个完全不同层次的差别。团队每编写多一个测试用例,都需要对代码进行必要的重构与解耦,知识与经验的积累就多一分。

让团队成功坚持下去的一个关键因素是,贯穿整个过程,必须有人(我)为团队提供了全方位的培训、指导、搭建框架与示例、以及一对一结对工作。直到团队成员拥有独立完成的能力。

团队成员的成长也非常明显:
从一开始,提的问题是 “这个功能我该怎么做?怎么写这个测试用例?”;

到后来,提的问题是 “这个功能我可不可以这样做?这个测试用例这样写对不对?”

再后来,提的问题是 “这个功能我有A B两种做法,哪种好一点?”

再往后,问题变成 “这个功能我有 A B两种做法,分别的优缺点是XXX,是否有正确,该选哪一个?”

最后, “这个功能有A B两种做法,分别的优缺点是XXXX,我觉得 A 更适合我们....”

这个阶段大约经历了半年左右,技术是通过不断的实践积累而成,没有一步登天的事情。

.


能力提升第三阶段

提升目标

  • 引入代码风格检查工具,并集成进CI
  • 团队一致认为单元测试确实有效,推进到功能测试、集成测试、系统间全流程测试,开始尝试TDD
  • 实现CD,由人工部署改进为自动部署

成果

  • 代码风格一致性高,且遵从最佳实践写法
  • TDD使得代码更进一步简洁,交付质量得到进一步提升,低技术债务, 高重构意愿
  • 缩短代码提交到部署的等待时间
  • 提升系统的部署运维的友好能力

我们拥抱XP的理念,“如果一个方法是有效的,那就努力把它推向极致”。
单元测试让我们体验到质量提升的同时,保证工作的成本却在下降,且效率在提升。因此,团队便编写更多的测试代码,使得人工测试的工作尽可能的减到最少,编写测试代码也成了团队基本工作一部分。

先写实现再写测试已经没有什么挑战和难度了,开始持续引导开发人员,能否尝试先写测试用例,再写实现?经过一两个sprint的尝试,开发人员适应了先写测试在写实现的方式。

注: TDD有一个小陷阱,是多数开始尝试TDD的团队都会遇到的,也是导致许多TDD持续不下去的重要原因。在我的内部分享<<TDD的正确姿势探索>>专门讲了这个问题,后期有时间再分享出来。有兴趣可以加我微信探讨 😉

除了功能质量之外,代码的风格、一致性、最佳实践方面也是我们想要解决的问题,通过引入代码风格检查工具 rubocop, 确保代码严格遵守规范,依照最佳实践.

这两种实践,令到团队在编写更简洁的代码和更优的交付质量的同时,获得更高的重构意愿: 开发人员表示,他们看到以前写的代码不够好的时候,他们很放心的就直接重构了,因为已经有充足的测试用例在一两分钟内确保重构是工作的。Rubocop又会帮助发现代码的臭味写法指导开发人员提升代码能力,以及可能的代码风险.

另外,部署工作由手工脚本化部署 (手动执行capistrano), 转为通过由gitlab pipeline自动执行,除了生产环境外,代码一旦合并,就会自动部署到相应的环境,节约了需要等到人工部署的等待时间。


能力提升第四阶段

提升目标

  • 验收标准的自动化
  • 通过docker容器化,统一环境,提升部署能力

成果

  • App自动化测试
  • 统一运行部署环境,缩短新服务器的准备时间

当团队做到TDD时,测试用例都是针对后端服务,API与业务逻辑的质量得以保证。然而,前端部分(ReactNative App)还是依靠人手测试,繁琐低效且耗时,难以做到每次发布都进行全回归测试。几个Sprint前端部分都存在这样那样的质量问题,而且随着功能的增加,出现的问题越多。

实际上,前端部分的质量问题,已经困扰我们多个项目很长一段时间了,一直寻找解决的办法,这次正好碰到一个契机,详见 敏捷实践 (1) - 我们是如何自动化App验收标准 ,我们解决了这个难题,前端部分也引入了自动化测试 ,做到了前后端测试的全自动化。每次交付,业务流程方便基本没有缺陷。

而且,另一个重大的收获,就是验收标准(Acceptance Criteria)测试自动化,形成Scrum中 用户故事 -> 验收标准 -> 测试自动化 的闭环,整个团队充分理解了验收标准的真正作用与价值,也是团队Scrum实践取得成效的重要基石。(有些团队Scrum实践的更多是流程,最后也就容易留于形式,收效远低于预期)

在部署方面,我们通过自动化脚本部署,简化了每次部署的工作。但是我们还有一些问题需要改进:

  1. 开发人员的环境与部署环境的差异,可能导致一些环境依赖问题。
  2. 每增加一台服务器,都需要4~6小时左右的初次准备时间。
  3. 每增加一套环境,都需要花费更多的时间和脚本修改。
  4. 弹性弱。

因此,对系统架构进行一些调整,让它能以docker的方式运行,解决环境一致性问题,准备一个新服务器的时间,也仅需不到一个小时左右便可。


能力提升第五阶段

提升目标

  • 进一步云化部署,Docker Swarm
  • 通过docker容器化,统一环境,提升部署能力

成果

  • 实现系统运维的弹性伸缩
  • 各个系统做到位置透明

这个阶段,主要解决docker化后剩余的问题: 仍是基于单个服务器部署。
继续调整系统架构与配置,支持以 Docker Swarm方式部署与运行,达到:

  1. 所有环境部署与运行方式都保持一致
  2. DEV & QA 等只需运行一个实例
  3. Prod 运行多个实例
  4. 实例位置透明
  5. 环境部署运行方式一致
  6. 自动弹性负载

待解决问题:

  1. DEV & QA的实例(Nginx MySQL/PostgreSQL App)被随机分配到某一个节点,导致查看日志前,需要先了解实例是运行在哪个节点上,造成很大不便。
  2. 生产运行多个实例,访问在实例之间自动均衡,日志查找困难。

做了这些,我们在哪?

image.png

团队的状态

对于开发团队来说,需求、开发、测试不再是割裂的充满苦与泪的工作,而是可适应可挑战的,也让大家避免了恶性加班的泥潭,工作之外还有丰富多彩的生活。

image.png

夜晚能安然入睡,不会苦逼的被半夜叫起来重启系统解决问题 😜.

image.png

总结

我们取得的成果

  • 在无需加班的情况下,产出的价值 > 原来加班
  • 低技术债务,我们能安然入睡
  • 正向循环, 团队在持续提升能力,从而贡献更多的产出
  • 不断探索可能的技术,感受快乐

我们依然有很多不足与挑战

  • 如何更彻底的云化 (k8s, service mesh, serverless?)
  • 如何应用Event Sourcing + CQRS更好构建系统
  • 如何更好寻找TDD与业务发展的平衡
  • 如何做到 持续部署 (Continuous Deployment)