用敏捷开发避免技术债务

Posted 湖畔随想录

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了用敏捷开发避免技术债务相关的知识,希望对你有一定的参考价值。

技术债

定义


技术负债(英语:Technical debt),又译技术债,也称为设计负债(design debt)、代码负债(code debt),是编程及软件工程中的一个比喻。指开发人员为了加速软件开发,在应该采用最佳方案时进行了妥协,改用了短期内能加速软件开发的方案,从而在未来给自己带来的额外开发负担。这种技术上的选择,就像一笔债务一样,虽然眼前看起来可以得到好处,但必须在未来偿还。软件工程师必须付出额外的时间和精力持续修复之前的妥协所造成的问题及副作用,或是进行重构,把架构改善为最佳实现方式。

1992年,沃德·坎宁安首次将技术的复杂比作为负债。


以上引用自维基百科。

形成


那技术债是如何形成的?技术债形成的原因很多,首先是一些一眼就能看到的浅层次原因:


  1. 用不好的程序员来写复杂的功能。

  2. 项目前期跑的太快,代码手工简单验证之后就匆忙合入。

  3. 没有代码规范,每个人都按照自己的习惯写代码。

  4. 所有人都有代码合入权限。

  5. 没有代码 Review,或者不严格执行代码 Review。

  6. 开发人员之间分工不合理,有重复劳动,或者有三不管地带。

  7. 项目人员之间沟通不畅,对需求的理解不一致,但没有人发现,或者发现时代码早已合入。

  8. 为了求快没有写单元测试,或者只写了几个简单 Case 糊弄一下了事。

  9. 没有自动化的回归测试。

  10. 为了方便,添加一些短平快的接口,破坏了系统架构。

  11. 拉分支,同时维护一个以上的代码分支。

  12. 代码提交备注写的非常随意。

  13. 测试人员和开发人员只靠邮件和测试报告沟通。

  14. 只有测试人员真正在使用所有的功能,开发者只保证自己的部分可以跑通。

  15. 迭代速度呈现指数型下降趋势,越来越慢,最后完全停滞。所有人都疲于解Bug,而解决一个 Bug,会带来三个新 Bug。这时候会发现,维护成本远远大于开发成本。

  16. 加班严重,项目参与人员疲于奔命,效率低下。

  17. 项目即将 Deadline,无法进行代码重构。或者代码重构后,进入下一个轮回。

图1 困于技术债的程序员


造成这种浅层次原因的是一些深层次原因,很多是跟公司的管理制度,人事制度,组织形式有关的,大概举几个例子:


  1. 流程复杂。一件很简单的事情,每一步都是高标准,严要求,每走一步都很费劲,然后却因为要求太高,或者实现流程本身的工具有问题而难以实施。比如代码提交的准入系统,集成部门制定了非常复杂的流程,然后工具匆匆上线,难用又很不稳定,导致一个提交总是失败。流程中需要各种权限确认,但一些权限只有极少数人有,而且人很分散,每一步都需要先找到对的人,等他有空了帮你把事办了,这样一件小事就变成了无尽的找人和等待,反而完全没有精力去关注事情本身。这样程序员用于写代码的时间被大量压缩,最后只能通过“快速而混乱”的方式先写完代码交差了事。


  2. 加班文化。对很多纯属还债的加班是鼓励的,不去反思怎么去减少这种本可以避免的劳动。这样大家把负债和还债当成了一种习以为常的现象,经常加班的人被奖励的更多,得到升迁的可能性更大,这样从进化论的角度讲,整个公司慢慢的就充斥着通过这种方式上来的管理者,从惯性和心态上还是支持加班文化。


  3. 部门墙。各个部门维护自己的利益,跨部门流程设计跟不上业务发展,或者各部门之间互为鲶鱼,互相拆台大于互相合作。这样,一套代码各有各的想法,无法形成统一的设计,流程上也各有各的不同,无法做到同步。


  4. 异地办公。除非是非常专业的工程师(比如 Linux 内核开发组的成员),否则异地办公就是灾难。

发现

  • 团队成员的言语中就可以发现代码的“坏味道”:

“这块代码还是别动了,可能导致其他模块不工作。”;

“只有小陈可以改这块的代码。”;

“我的代码在我的分支上,已经很久没有合入主干了。”;

“这个功能啊,上一个版本貌似还是好的,这个版本不知为啥有点问题。”


  • 偶尔扫一遍代码,就能发现一两处 Coding Style 问题。


  • 开发速度下降。那说明利息开始利滚利的增长,到了影响本金的量级了。


  • 测试覆盖率没有达到90%。低于90%的代码测试覆盖率,会导致很多潜在的问题得不到早期的暴露。


  • 代码中有长期存在的 TODO 和 FIXME。

避免

如何避免技术债,这个话题有点太大,本质上是软件工程这门学科需要解决的根本问题。我也根据自己的经验抛砖引玉一下:


  1. 招聘好的程序员。

  2. 重点不是去招聘更多的程序员,而是努力让现有的程序员更有效率。

  3. 严格的代码 Review 制度。

  4. 严格执行小型测试,中型测试和大型测试。

  5. 重要性:测试代码 > 工作代码 > 代码注释 > 文档。

  6. 在代码没有通过严格测试之前无法合入。

  7. 对于提交时的备注,摘要应精炼如微博,描述应详尽如博客。

  8. 持续集成,理想状态下是每次提交后的质量都达到可发布状态。

  9. 代码持续重构,写自动化测试保证重构的安全和效率。

  10. 给所有人看 Demo,而不是各种 PPT 和邮件报告。


但是这些方法都太零碎,太不成体系了,要让一个团队交付的代码质量得到根本上的提升,需要一整套行之有效的流程,方法和工具。

有一个答案,就是已经流行很多年的敏捷。在这里,不提敏捷的教条,我只讲我自己理解的敏捷是什么。

敏捷

什么是敏捷


首先,敏捷是一种过程控制论。通俗的说,就是一种做事情的方法。


  1. 它适用于软件,因为软件是软的,可以改。要是硬件,改起来就没那么方便了。

  2. 它适用于客户不知道自己要啥的情况,其实这样的客户占绝大多数。因为客户不知道要啥,所以你需要不断帮客户弄明白他到底想要啥。换句话说,你需要和客户沟通,合作,倾听反馈,持续改进。

  3. 它适用于竞争激烈的市场,这样的情况下,赶在竞争对手前交付一个不完美但至少能用的产品非常重要。

  4. 它适用于快速变化的市场,你在埋头造一辆汽车的时候,客户已经想开飞机满天飞了,这就需要你能一步步的把汽车改成飞机,还能按时交付。

  5. 它适用于在一个地方办公的小团队,一般10个人以内。这样能使敏捷中主要的沟通方式 “Face to Face” 是可行的。


其次,敏捷开发是一套工具集,里面有形形色色的工具。你可以不搞敏捷,但可以用那么一两个来提高工作效率。


比如:

  1. 站会:三个问题(昨天做了什么,今天要做什么,我有什么需要帮助),简洁有效的小团队沟通方式。

  2. 看板:直观反映工作进度,反映流程遵守情况,找到瓶颈所在。

  3. 计划,演示,回顾会:适合于小团队的协作和优化反馈方式。

  4. 用户故事:站在用户的角度讲需求。

  5. 持续集成:随时高质量交付的基础,有利于应对变化剧烈的市场。


最后,敏捷开发是一种企业管理方式。


  1. 一线员工可以同时是架构师,SM (Scrum Master),开发工程师,测试工程师,发挥了他的主观能动性,有利于创新和效率。

  2. 敏捷不专注于敏捷团队中个人的绩效考核,而更多的侧重于整个团队的绩效,更好的避免了 KPI (Key Performance Indicator) 驱动模式。

  3. 把大项目拆分成小项目去做(每个 Sprint 都是一个迭代,需要输出一个高质量的版本,相当于完成一个小项目),把 Bug 的生存期控制在一个迭代以内,降低了风险,也减少了后期改 Bug 的工作量。

  4. 把数十人的大 Team 分成几个敏捷团队,这几个敏捷团队的 SM/ PO (Product Owner) 再组成一个更高一级的敏捷团队,利用站会,反思会,看板等等敏捷元素,可以避免数十份邮件也不能解决一个小问题,大家互相踢皮球,沟通不畅的大企业病。

  5. 老板可以是最大的 PO,他给下面的高管讲 Idea (User Story),定期检查 Demo,把控产品用户体验,负责和外界的沟通合作,比如乔布斯。

用敏捷开发避免技术债务

图2 乔布斯和他的IPod


宣言和准则


敏捷有几个大家耳熟能详的宣言和准则,我挑几个重要或容易有歧义的说明一下自己的见解。


  1. 个体和交互胜过流程和工具

    好的程序员不是万能的。高手程序员就像NBA明星球员,他们个人能力很强,但一个全明星团队,如果没有很好的合作方式的话,大概率会输给一支水平一般但合作很好的球队。


    交流最重要的方式是面对面交流。面对面交流能带来丰富的信息量,人和人交流的信息量里面,肢体语言占70%,表情占20%,而内容仅占10%。另外当面交流不仅交流了信息,也建立了团队成员之间的信任感。


    流程和工具是为“面对面交流”查漏补缺的,是为“面对面交流”服务的,而不是反过来要求人去服务流程和工具。流程和工具需要去节省人的时间而不是去侵占人的时间。


  2. 代码即是文档

    其实这个不是说不让写文档,而是含有两个意思:首先,把代码当成文档一样去写,这样的话,你就能写出自解释的代码了;其次,不要写跟代码一样具体的文档。文档要短(几页甚至一张图即可,方便理解设计思想),高度概括。


  3. 代码公有制

    很多公司为了减少分配工作的麻烦规定了模块的所有人,就是说一个模块有一个或几个人专门负责,别人无权干涉。其实这样是很有问题的,这会人为的造成了很多开发中的互相等待,也会导致一些固步自封的设计。应该设置模块守护者,而不是 Owner,守护者必须 Review 相关模块的代码。

需求管理


敏捷不支持 PM 直接找 RD (Research and Development) 提需求,这样看起来解决问题快,但是需求来源非常分散,往往无法经过统一的设计,实现后容易影响架构。还有一个问题是 RD 容易被打断,要知道,越是智力密集型劳动,打断的成本越高。敏捷需要统一透明的需求管理,统一指的是只有一个需求输出口和对接口,透明指的是所有人能看到所有需求。


排优先级的时候,不能使用重复的数字定义优先级!因为如果可以采用同样的数字的话,那很多任务都会被定义为最高优先级。而且采用不同的优先级,大家需要做什么事情在任何时刻都一目了然。

用户故事


用户故事是从用户的角度来描述需求,一般格式是:


作为一名用户,我因为某种“需求”,想要一个“结果”。


例子:作为一名手机用户,我想要快速进入语音交互,因此需要语音唤醒语音助手。


用户故事要辅以测试用例,测试用例是需求的精确描述版本。比如上面的用户故事,测试用例就要精确描述“快速”:从测试者说完唤醒词开始,到语音助手界面跳出结束,使用的时间不超过200ms。


测试用例要经过 PO Review,或者干脆由 PO 来写,因为 PO 是提需求的,而测试用例才真正精确描述了需求本身。


用户故事偏 UX (User Experience),如果完全没有 UX 的功能,可以不用写成这种形式。可以写成“支持北京的车牌号码识别”等。


把一堆复杂的需求拆分成 User Story 是比较有挑战的事,有一个经典的 INVEST 原则:


  • Independent:独立性。一定要保证 User Story 在功能上的独立,尽量不要出现一个等一个的情况。

  • Negotiable:可谈判性。RD 可以和 PO 进行适当谈判来调整这个 Story。

  • Valuable:有价值性。 Story 需要体现出对于用户的价值。

  • Estimable:可估计性。Story 应可以估计出 Task 的开发时间。

  • Sized Right:大小合适。建议的工作量是2-5天。

  • Testable:可测试性。


在实践中,拆分 User Story 遇到的最大挑战是对于复杂的项目,由于需求的不确定性和技术上的不确定性,很难在早期就把设计完全想清楚,这时候怎么做呢?我们可以想象一个人走夜路,他只能看到面前几米的距离,那他需要做的就是先(鼓足勇气)走完这几米,这样他就能看到更远的几米了。对于产品开发来说,此时要把一些已经看清楚的功能做起来,并达到整个产品可 Demo 的程度,当所有人(包括客户)看到这个 Demo 的时候,下一步做什么就是自然而然的事情了。

用敏捷开发避免技术债务

图3 走夜路


这就需要把开发任务竖着切分而不是横着切分。比如下图,最终的产品是由业务逻辑,SDK (Software Development Kit),算法三部分构成,传统的做法是分别想明白这三块,定义好接口,分别开发,最后集成测试。但是更好的做法是:把算法,SDK 和业务逻辑中的一部分“需求和技术都很明确的功能”拿出来,做一个可以 Demo 和测试的产品,作为 Sprint 1 的交付目标,等到 Sprint 1 接近尾声时,再设计 Sprint 2 的交付目标。


用敏捷开发避免技术债务

图4 像切面包一样切分任务

看板

看板的形式多种多样,可以是一块白板,也可以就是一面玻璃墙。网上也有一些电子看板,但我觉得实物的更好,这就跟面对面说话比用电子邮件好是一个道理。


为什么需要看板?


  1. 项目进度一目了然。看板就是项目当前情况的 Big Picture。

  2. 每个人的行动指南。一个 User Story 的生命周期被切分成较小的块,每个人应该只工作在其中一块。

  3. 找到瓶颈。如果你发现大量纸条贴在了代码 Review 一栏,你知道是时候调整一下 Reviewer 的时间安排了。


站会和看板,很大程度上利用了人的荣誉感,这也是项目进度可视化的题中应有之义。


看板的设计还是很有讲究的,有简单有复杂。简单就是三个状态,Undo,Doing,Done,好处在于花在看板维护上的时间会降低,但坏处是没有办法去规范流程。我比较喜欢复杂一点的看板,能够把关键的流程可视化。


在敏捷的各种原则中,看板和站会是性价比最高的,你不需要投入就会有很好的收益。而有一些原则,比如自动化测试,需要前期大量投入才会有后期很好的收益。

用敏捷开发避免技术债务

图5 玻璃墙看板


下面是一个看板的例子,比较有普适性:


需求 => 设计 => 编码 => 代码审查 => 测试 => 文档 => 完成


我们可以看到,User Story 的生命周期被分成七个阶段,就像升级打怪一样,从一个阶段到下一个阶段必须满足设定的条件:


需求 => 设计:


  • 团队成员选择合适的 User Story,成为Owner


设计 => 编码


  • Owner对这个 User Story 进行技术上的设计,并把设计分享给相关人,得到认同。如果团队有架构师的话,需要架构师点头。


编码 => 代码审查


  • 完成 User Story 和小型测试代码

  • 通过编译

  • 通过功能自检

  • 通过小型测试

  • 通过内存泄漏检查

  • 通过测试代码覆盖率检查(根据开发环境和进度可选)

  • 邀请至少两人进行代码审查


代码审查 => 测试


  • 通过 Code Style 自动化检查

  • 通过所有自动化测试(小型测试,中型测试)

  • 通过人工代码审核,包括测试代码。注意重要性从高到低:测试代码 > 功能代码 > 注释 > 文档


测试 => 文档


  • 通过测试工程师的手动测试,主要为根据测试用例验证功能实现

  • 合入代码


文档 => 完成


  • 如果有文档要求的话,通过文档审查,合入文档

测试


关于测试种类的命名多种多样,有黑盒测试,白盒测试,单元测试,模块测试,集成测试,回归测试等,为了不引入误解,我们先从测试的规模上进行分类,事实上这也是谷歌对测试的分类:小型测试,中型测试和大型测试。


小型测试


  • 用于验证单独的函数,着重于功能正确,错误分支,边界条件(如 Off-By-One Error)等的检查。

  • 都是自动化测试。

  • 小型测试要求运行时间非常短,最好在1秒之内完成。

  • 小型测试由代码的开发者编写。


中型测试


  • 用于验证一个以上模块的交互,着重于功能正确,稳定性,性能,内存泄漏检查。

  • 都是自动化测试。

  • 中型测试要求运行时间比较短,最好在5分钟之内完成。

  • 中型测试有测试开发工程师或开发工程师编写。


大型测试


  • 用于检查端到端的交互,着重于验证软件是否满足最终用户的需求。

  • 包含自动化测试和手动测试。重复性的工作最好都编写进自动化测试用例,比如录音,定位点击等。手动测试主要为了弥补自动化测试用例的不足,以及对产品的用户体验的整体评价。

  • 含压力测试(长时间运行的稳定性测试,组合不同的硬件环境)。

  • 大型测试可能需要时间也许会达数小时之久。

最佳实践


编码阶段的测试


  • 把小型测试加入 CMakeList 或编译脚本,每次本地代码编译的时候触发。


构建版本后的基线测试


  • 此时代码处于提交但未合入状态,由 Jenkins 进行版本构建,并调用脚本执行,包括:小型测试,中型测试。


基线测试后的功能验证


  • 【手动】测试工程师根据合入的 User Story 和对应的测试用例确保功能正确实现。通过这一关代码才可以合入。


每日构建版本测试


  • 小型测试。

  • 中型测试。

  • 大型测试中的自动化用例。

  • 【手动】手动对新加功能进行测试。

  • 【手动】手动功能回归测试,可以逐渐把一些标准化 Case 加入自动化用例。

    注:手动测试可以选择每天在一种硬件环境中执行。


持续构建版本(可选)


  • 如果觉得24小时测一次还是太长,可以持续不断的在服务器上滚动构建版本进行测试,一旦 Case 失败,则发邮件给最新合入代码的人,他们是最大的嫌疑人。

  • 滚动执行上面的每日构建版本测试。


发版测试


选一个通过每日或持续构建的待选版本,然后:


  • 组合不同的硬件平台,进行全面压力测试。

  • 【手动】执行所有手动测试用例。

  • 【手动】验证本版本所有已修复的最高优先级和次高优先级bug。


请注意上面标有【手动】二字的条目,可见需要手动测试的内容并不多,测试工程师的压力不算大,主要的测试工作由测试开发工程师(其实也可以是开发工程师)完成。

一些TIPS


  • 测试代码也需要测试,重要性上:测试代码 > 工作代码 > 代码注释 > 文档。


  • 测试用例必须是可独立运行的,可随意切换顺序正确运行。


  • 测试代码不能做任何数据持久化的工作,保证测试用例离开环境的时候能保证不改变任何东西。


  • 大量重复性的工作不适合手动测试,切换成自动化测试。


  • 不要在端到端的自动化测试上投入过多,因为这种用例和业务逻辑紧密耦合,通用性不好,性价比不高。应该把自动化的重心放在基本功能的实现上。


  • 测试工程师只是一个角色,不是固定一个人,和开发工程师可以互换。


  • 测试和开发一定要坐在一起!这个非常非常重要,要面对面沟通,可以用JIRA之类的工具记录 Bug 流转流程,但更重要的是面对面的交流。


  • 测试者和开发者要交错工作,不要互相等。开发者开发相应功能的时候,测试者在写测试用例(工作的 DoD,Definition of Done),开发者完成功能的时候,测试者就可以测试了。

各种会


很多公司不喜欢敏捷的一个原因是会比较多,但其实这是一个误解,由于面对面沟通的不可替代性,开会还是人类最高效的沟通方式。而且敏捷的会有两个好处:


  1. 对于敏捷团队来说,这些会是固定时间的,不会突然袭击,也就不会打断工作思路。

  2. 这些会对于项目来说是足够的,不需要再开其他会。


敏捷的会主要有:计划会,每日站会,回顾会,展示会,按优先级从高到低排列。

计划会


计划会是用来规划一个 Sprint 的工作内容和认领工作任务的,必须开。要确保:


  1. 工作内容满足项目进度。

  2. 合适的人完成合适的事情。

  3. 所有的事情都有人做,不存在重复劳动和三不管地带。


注:Plans are nothing, planing is everything.

每日站会

站会的目的我就不再赘述了,它的实质是透明化开发进度,提出问题。


以下是一些不好的站会行为和解决办法:


  1. 迟到。解决方法是制定一些小的惩罚,比如发个红包。

  2. 超时。超时的很大原因是把站会当成解决问题的地方了,但它是提出问题而非解决问题的,真正的问题需要站会后分成小团体各自搞定。也可以采用一些比较极端的方法解决超时问题,比如下图:

    图6 平板支撑“站”会


  3. 无意义的发言。比如我昨天在解 Bug,明天还要解 Bug。需要 Scrum Master 去提示:什么样的 Bug?原因是什么?有什么经验教训可以提醒大家的?

  4. 轮到自己了才发言,不轮到自己就玩手机,只有 SM 全程在听。解决办法是轮流主持会议,比如今天算法,明天工程,后天测试等等。

  5. 只是向 SM 汇报工作。解决方法是告诉团队要广播,是全团队沟通。

展示会


一般在一个 Sprint 的末尾,主要就是 Demo 完成的 Story,PO 检查是否确实完成了。其实我觉得仅仅在展示会上看 Demo 不是一个好的实践,对于 PO 来说,Demo应该是每天都要看的。这个会可以取消,也可以开的很简单。

回顾会


这个也很重要,通过总结经验教训来优化流程,为了不开成批斗会,可以准备一些零食,开得活泼一些。

图7 批斗大会

一些名词

结对编程

分成两种方式:

  1. 知识传授,大牛带小牛。

  2. 俩牛人,写有难度的代码,一个驾驶员,一个领航员。俩人用一个电脑,用俩键盘,俩屏幕,但同一时间只有一个人输入。结对,是利用了左右脑的优势,左脑关注细节,右脑关注 Big Picture,那两个人正好分工。两个人可以换着写,这一个小时你是驾驶员,下一个小时我时候驾驶员。

极限编程

就是把一些亲测有效的方法用到极致,比如 UT (Unit Test) 有效,那就 TDD (Test Driven Development)。

精益开发

指查明和消除浪费。什么叫浪费?无法交付的东西都是浪费。



以上是关于用敏捷开发避免技术债务的主要内容,如果未能解决你的问题,请参考以下文章

技术债务揭秘

技术债务墙:一种让技术债务可见并可协商的方法

云原生计算能消除技术债务吗?

技术债务(Technical debt)的产生原因及衡量解决

技术债务和技术投资

翻译: 技术债务墻:一种让技术债务可见并可协商的方法