关于软件编程思路的一点借鉴

XP: eXtreme Programming

做好大公司下的软件工程和项目管理不是件简单的事,业务变更总是超出预期,人力永远跟不上业务规模,团队成员能力参差不齐,代码工程日渐发臭,直到积重难返无能为力,只能花更多心力勉力支撑。这在大公司里可能是很容易遇到的一些场景。它不仅关于编码,是更大的技术甚至管理命题。要解决好,要向外求向内求。向外求,看看其他人是如何做的,这就是最近看的这两本书的初衷。

  • 《敏捷整洁之道》
  • 《Google软件工程》

敏捷之道

极限编程(XP)、敏捷开发在大公司的软件开发中能见到一些残影。敏捷是什么?用鲍勃大叔的话说,就是帮助做小事的小团队解决小问题的小主意。小而美。小步迭代,快速试错,快速反馈。那敏捷能解决大团队的大事情吗?当然不能。这需要敏捷以外的手段。在明白这个前提下,就可以看后文了。敏捷虽然“”,但小里面也能吸取一些经验。

了解敏捷

敏捷前是流水线式的瀑布管理流程,它的僵化流程不重要,只需要知道它给团队和程序员带来了一些麻烦,也影响了许多程序员的思维方式。而后极限编程XP出现了,再之后一帮程序员在雪鸟会议上提出了敏捷宣言,宣言倡导4条价值观

  • 个体和互动优于流程和工具
  • 工作的软件优于详尽的文档
  • 客户合作优于合同谈判
  • 响应变化优于遵循计划

在这个价值观下的实操,将会是类似下面这样的。

  • 反馈驱动,用sprint作为项目的子周期单位,用燃尽图来度量进度和发现问题,可以祛除幻想用数据说话
  • 项目管理铁十字:质量、速度、成本、完成。
    • 快速前进的唯一方法就是做扎实,生成垃圾代码不会使你更快
    • 给delay的项目增加人手反而会导致更加delay
    • 调整功能优先级,削减功能

更具体的,“生命之环”在业务、团队、技术上提供了一些具体的框架

  • 业务实践:计划游戏、小步发布、验收测试、完整团队
  • 团队实践:可持续节奏、代码集体所有、持续集成、隐喻(DDD)
  • 技术实践:简单设计、测试驱动开发、重构、结对编程

框架里的每一项都和价值观相关联。

敏捷的理由

敏捷的重要性在于保证自己的专业性和给客户合理期望,即高质量交付、持续交付、高质量架构设计来保证稳定生产率。同时开发人员和客户划分清晰权利条款,这部分不细展开。因为职责划分和公司里的组织架构、文化、项目开展形式密切相关,书里给出的无法直接使用。

业务实践

生命之环的业务实践部分主要是一些流程上的建议。譬如

  • 计划游戏:将需求拆成故事点,按ROI排序,用几个简单的迭代周期评估整体完成的风险
  • 小步发布:结合测试,频繁地发布集成
  • 验收测试:业务方编写形式化的用户行为case,开发人员来实现测试自动化,QA来做悲观测试
  • 完整团队:同地办公,鼓励沟通

团队实践

这部分主要是一些团队协作的建议。

  • 隐喻:即领域模型设计的思路,建模问题域,定义业务元语来对齐认识
  • 可持续节奏:被动加班并不能体现奉献精神,只能表明计划做得糟糕,充足的睡眠很关键
  • 代码集体所有:知识共享,不意味着不能有所专长,但也要是通才
  • 持续集成:CI/CD永不应该破坏,所有测试一定都要通过
  • 站会:只过做了什么?将做什么?需要什么帮助?不要深入讨论,只呈现事实

技术实践

这部分是具体在技术领域的建议。也是三类实践里的基本,缺少技术实践,将无从谈起敏捷。

  • 测试驱动开发(TDD):先写失败的测试,再写让测试通过的代码,保证代码一直可以工作
  • 重构:重构只改变代码结构,不改变测试定义的行为;重构永远不出现在时间表里,它是我们每分钟、每小时开发活动里不可分割的
  • 简单设计:减少认知负担
  • 结对编程:间歇性、不强迫,来增进知识共享

实践敏捷

敏捷的价值观包括勇于冒险、积极沟通、快速反馈、直截了当,保持团队和代码简单。说起来就是这么简单,但组织像敏捷转型并非易事,有时需要团队内的敏捷教练来倡导和监督。在大型组织里,更是需要敏捷以外的手段来解决问题。敏捷对团队的要求实际上挺高,团队需要较高的技术和工程实践能力,需要战略性思维,需要模块化设计。

书里有一句话说得很好:“没有实践的原则只是空壳,没有原则的实践往往是没有判断力的死记硬背。原则指导实践,实践具象化原则,两者齐头并进。”。如果在没对问题达成一致前就提供了解决方案,人们看不到价值,就不会改变他们的工作方式。

软件工程

这本书的内容不涉及具体的代码编写,而是聚焦工程,主要在讲软件工程的方法论。从理论到文化到流程再到工具。针对大公司里遇到的工程问题,给出了解决的视角和思路。这探讨的范畴恰好和敏捷是另一个方向,一个是大而壮,一个是小而美。既然聊到工程,主要就是从下面的维度来分析问题。

  • 时间维度:代码的生命周期里如何适应变化
  • 规模维度:组织和工程在增长中如何适应
  • 成本维度:如何权衡成本来做出决策

理论部分

首先,软件工程不是编程。它是带有时间维度的编程。要考虑上面提到的三个维度,一方面做到软件的可持续性,即响应持续的有价值变更的能力;另一方面要能适应业务和团队的规模。在这么多因素里,要做好权衡。

  • 时间:关系着代码的生命周期。大公司不同于个人的练手项目,项目的生命周期长很多,甚至长到无限;因此要思考的不只是“现在可工作”,而是“一直可工作”。长期保持软件的可维护性是一项挑战。在“法无禁止即可为”的背景下,用户足够多时,什么写法都会发生
  • 规模:不只指团队的规模,也有业务的规模,扩展性不佳时,不仅在代码、在整个编码流程上都会遇到问题,例如API弃用、API升级、代码合并。借助组织规模化,知识分享也可以带来超线性的价值。同时风险左移也可以降低维护成本。
  • 权衡:重要的是“达成共识”,而不是“我说了算”。决策是需要经过充分讨论的,有开放和明确权衡的,而不是拍脑袋决定的。一个数据驱动的文化可以支撑决策,在数据改变时及时调整方向。

总结一下,编码是产生代码,软件工程是代码维护,是一组政策、实践、工具,是有管理成分的。

文化部分

团队协作

一切为了团队

文化少不了人,在公司里,代码维护都是团队协作的成果。软件工程是团队努力的结果。一个良好的团队氛围很重要。

  • 围绕团队而不是个人
    • 团队成就感 > 自我感觉良好
    • 设置人员backup,提升巴士系数(开发者被巴士撞了让项目停摆)
    • 团队反馈,“足够多的眼睛可以让所有问题暴露”
  • 谦虚、信任和尊重
    • 无指责文化、建设性批评
    • 虚心提出和接受批评
  • 多元兼容的团队
    • 兼容用户
    • 兼容团队成员

知识共享

组织需要知识共享来降低沟通成本。因此需要创建一种促进开放、诚实的知识分享文化。

  • 必要性:大公司、流水线式组织必然会有信息碎片化、信息重复、信息偏差的问题,那些存留在单个成员大脑里的未文档化的知识孤岛很容易逸失
  • 促进手段
    • 鼓励持续学习和交流
    • 鼓励社区提问和解答
    • 技术讲座、技术分享、尊重、激励和奖赏
    • 建立规范的信息源
    • 去改变事物之前,先了解它为什么在那里
  • 可读性代码阅读量要远远大于书写量

团队领导

大小团队的领导风格是不一样的。

  • 基层领导:偏业务的经理(Manager)和偏技术的技术主管(Tech Lead),或两者兼有。
    • 克制住管理的冲动,学会营造氛围,做好服务工作
    • 关注团队的健康和成长,不操心如何完成任务,而是完成什么任务
    • 关注人
      • 招聘比自己强的人
      • 重视低绩效的人
      • 有人情味
      • 不要混淆友谊和工作
    • 正面case
      • 多信任,少微操
      • 对其目标,留下空间
      • 当禅师,不给解法,多引导
      • 多做拉齐共识的事,帮助成员解决资源的障碍
      • 坦诚、有同理心
  • 大团队领导:多做决策,培养自驱,考虑扩展
    • 多做决策:定义清楚问题,识别盲点,权衡和决策
    • 培养自驱:建设自组织、自管理的团队,组织里有一组强大的领导者,健康的工程流程和积极自驱的文化,增加自己的可替代性。达到这点,需要做几件事
      • 划分问题子方向,给团队清晰的目标感和成就感
      • 授权问题和领导者,培养一批自立的领导者,多思考“我能做什么团队其他人做不了的事情
      • 调整和迭代,保持自驱团队的健康,谨慎锚定一个团队的角色,要随业务发展
    • 考虑扩展,随着能力的提升和成功,责任和问题会越来越大,要学会授权,保护自己的精力,更多关注最重要的事情

度量

首先团队必须保持数据驱动,减少主观误判。不仅要提高生产力,还要高效做到这一点。度量前,先要认识以下几点

  • 确定度量指标足够反映问题,而不是虚荣心指标
  • 确定的目标要能针对结果采取动作
  • 确定自己有精力变更流程/工具

在目标上,可以从代码质量、工作专注度、认知复杂度、速度、满意度几个方面来度量。然后针对这些维度确定指标。

流程部分

代码风格

风格指南旨在提高一致性,提高代码对于时间和规模的韧性

  • 规则就是法律,是强制性的,不仅是建议或提示
  • 风格的原则
    • 为读者优化,而不是作者,推荐“简单读”而不是“简单写”
  • 规则的好处
    • 一致性
    • 可扩展性
    • 避免容易出错的写法
    • 和外界一致
  • 调整规则
    • 对于新特性,规则逐步放开
    • 规则不能一成不变,由社区提出,委员会决策批准
  • 配套工具:lint、格式化工具
    • 自动化工具的好处:公平公正、不易遗忘、可扩展性

代码审查

代码审查(Code Review,CR)是一种知识共享的重要工具,用好的话,也可以促进积极的团队氛围。

  • 流程上,可以要求代码的OWNERS,也可以邀请上专门负责可读性的人
  • CR就像论文的同行评审,可以大大提升代码质量:正确性、可读性、一致性,同时也能加深代码团队所有的观念,促进知识共享
  • 最佳实践
    • 礼貌而专业:信任和尊重文化,如果不同写法都有效果,且能通过可读性检查,应该接受作者的偏好
    • 鼓励小变更,可以减少CR成本
    • 在CR前,准备好清晰的变更描述
    • 减少CR人数目,提高自动化程度
  • CR类型:
    • 新项目
    • 改进需求
    • hotfix:避免夹带其他fix,会增加评审难度和回滚难度
    • 重构

文档

文档是知识分享重要的一环,但文档质量是所有程序员都感知过的通病,要想搞好文档质量需要一番功夫。

  • 文档的重要性:前期的投入换来后期的团队回报
    • API介绍可以帮助评估设计
    • 路线图和历史记录可以提供更多上下文
    • 减少其他用户的问题
  • 像代码一样对待文档
    • 有所有者和负责人
    • 有源代码管理
    • 有变更评审
    • 有缺陷跟踪
    • 有定期评估甚至测试
  • 最佳实践
    • 认清读者:初学者 or 专家,有目的的探索者 or 困惑者,客户 or 团队内部成员
    • 单一的文档目的:注释生成的参考文档、技术设计文档、新手教程、概念文档、landing page
    • 完整、清晰、准确、简洁
    • 5元素:WHO、WHAT、WHEN、WHERE、WHY
    • 文档的有效期

测试概述

开发人员自驱的自动化测试实践是重要的测试文化。

  • 自动化测试的文化是软件变更的基础
  • 测试细粒度:单元测试、中型测试、大型测试,以单元测试为主,集成和端到端测试为辅
  • 代码覆盖率的最大价值是对未覆盖代码的洞察,并不能代表覆盖代码没有问题
  • 自动化测试可以解放人工测试人员精力

单元测试

单元测试是写得最多的一类测试。可维护性的测试很重要,频繁阻塞发布的测试错误会降低开发者对测试的信任度。

  • 编写测试的最佳实践
    • 按用户路径,测试公共API
    • 测试最终状态(结果如何),而不是行为(做了没做)
    • 保持测试的完整度和清晰度,一个case只做一件事
    • 清晰的测试失败信息
  • 测试结构化:Given……When……Then
  • 测试代码应更清晰直接,而不是更聪明更高复用程度

大型测试

大型测试成本更高,但可以覆盖单元测试覆盖不到的地方,也有更高的保真度。

  • 大型测试的挑战:可靠性、快速、可扩展性
  • 大型测试类型:压测、UAT、AB、小流量、容灾演练、灰度用户等
  • 需要明确大型测试的负责人,否则相比单元测试,大型测试更容易劣化

mock测试

mock类型测试是实际实现的轻量级替替身实现,它对于提高测试case的清晰度和测试效率至关重要,但是滥用的话,会带来测试不清晰、稳定性差的问题。因此有一些最佳实践。

  • 在速度相近的情况下,实际实现永远优于mock实现
  • 几种mock类型
    • 桩(stub):模拟实现,返回固定值
    • 模拟(mock):模拟实现,返回可配置的值
    • 间谍(spy):记录调用信息,返回固定值
    • 伪造(fake):模拟实现,返回可配置的值,但是可以修改
  • 过度使用桩技术,会让测试代码脆弱不清晰,可以考虑用伪造技术。伪实现的内部是实际实现的轻量版,相比桩更拟真
  • 对写函数调用状态测试(结果如何),对读函数使用行为测试(做了几次)

弃用

对于一个明显过时的且有成型的替代产品的系统,最好的办法是弃用。代码是负债,产品功能才是资产。代码只会带来开发成本和维护成本,弃用正是从降低成本角度出发的。

  • 弃用比构建更困难
    • 使用用户多,迁移有难度
    • 说明价值困难
    • 最初的设计通常不会考虑弃用
  • 弃用类型
    • 建议型:只能起到宣传作用,不能指望用户完成主要的迁移
    • 强制性:要提供可操作性的手段,越早提示越好
  • 推进流程:要有owner,要有里程碑,要避免倒退

工具部分

版本控制和分支管理

软件开发和软件工程的重要区别之一就是,软件工程一定要有版本控制。本节的其余部分主要在推介主干分支(trunk-based)开发和monorepo。

  • trunkbase:长周期分支会带来维护困难
  • monorepo:有利于统一依赖

代码搜索

需要用工具帮助开发者理解代码和检索他需要的代码信息。

  • 强大的检索功能:各种检索场景
  • 可视化的UI

构建工具

构建是开发人员的重要工作之一,提升构建体验对开发者很重要。

  • 愿景:要快,要正确
  • 实现方式:基于制品 > 基于任务 > shell脚本 > 直接使用编译器
    • 基于制品相对于基于任务,只要解决好依赖的划分,可以很好地实现复用、并行化、增量构建,从而提升构建体验
    • 用hash去标识构建产物,实现缓存和避免“供应链投毒”
  • 构建技巧
    • 分布式构建:远程缓存、worker并发执行、按需下载
    • 模块依赖处理
      • 合理的模块拆分
      • 显式依赖传递
      • 单一版本规则
      • 显式外部依赖
      • 外部依赖本地化

代码评审

之前就有提过,代码评审是提高代码质量和知识分享的有效手段,但花在评审上的时间会占用编码,所以任何评审流程的优化都可以带来生产力的提高。

  • 一些最佳实践思路
    • 清晰的UI、简易的操作流程
    • 自动化提前发现一些问题
    • 集成到工作流中,如办公软件里

静态分析

静态分析类似lint,是代码问题左移的重要工具。

  • 最佳实践
    • 减少误报
    • 自动修复
    • 增加用户的误报反馈
    • IDE集成

依赖管理

依赖管理是软件开发里最复杂的一类话题。

  • 引入依赖可以提高开发效率
    • 慎重引入新依赖
  • 依赖管理本身就比代码管理难得多
    • 依赖网络随着时间推移越来越复杂
    • 兼容性问题和菱形依赖
    • 用CI和测试来保证
  • semver的局限性:来源于将软件变更简化为版本号变更,带来保真度损失的问题
    • 过分约束 or 过分承诺
    • 最小版本选择
  • 开源的隐患:外部用户维护成本、系统中任何可被观察的行为都会被依赖

大规模变更

大规模变更一般指一些基础设施的改造,需要调整整个仓库维度的写法。包括

  • 反模式清理
  • 替换已废弃的写法
  • 支持底层基础设施的升级改进
  • 旧系统迁移

这类事情最好有专门的团队负责迁移工作。原子性地进行大的变更很困难,需要拆成较小的、独立的快,这也给大规模变更带来的难度。一般至少要包含下面几步:

  • 提出变更提案,获得委员会授权
  • 创建变更
  • 切片并提交
  • 各切片的测试和评审
  • 提交

持续集成

CI是一个老生常谈的话题,在极限编程里也有提到,就是通过频繁地集成,减少项目风险的手段。

  • 关键:快速反馈循环
  • 重要部件:功能开关(feature flag)、自动化测试
  • 最佳实践:快速可靠的测试case、快速修复导致失败的变更、先回滚后修复

持续交付

持续交付是CI向终端用户的延伸。是保证小步快跑的实践,将小批量变更快速实现并交付用户。配套的工具有功能开关、发布火车、灰度发布、ab实验等。

计算即服务

计算资源池化是公司规模变大后的必然趋势,它可以提高管理资源的效率,同时能给软件提供标准化、稳定的抽象和环境。

  • 功能组件:机器资源调度、多租户隔离、容器
  • 选型:公有云、私有云、serverless
    • serverless是更轻量级细粒度更小,但定制性也更弱的方案,对规模较小的组织和团体吸引力较大