开发团队的自我救赎

作者: edwardzhq | 来源:发表于2019-02-03 17:06 被阅读59次

    前言

    前段时间受邀到厦门线下分享,后来也在众学院上在线分享这个主题。自我回顾,在有限的时间内,确实很难把整个主题全部详细分享。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)

    相关文章

      网友评论

        本文标题:开发团队的自我救赎

        本文链接:https://www.haomeiwen.com/subject/yqeckqtx.html