Programming Principle & Code Test
导言
大型团队项目代码的编写,有些要求和原则需要遵守。
项目闭环¶
流程¶
- 需求澄清明确:理解背景、功能/收益点、基本设计与实现思路。
- 人无我有,人有我优,人优我异。
- 竞争力 = 质量/成本/进度 三角;保证质量优先。
- 开发框架构建:
- 敏捷开发流程(快速编译涉及的小单元、快速测试小例子)
- 彻底理解工具(或许耗时,但有方法能彻底理解逻辑,e.g.,pytorch.profile,viztracer,GDB)
- 实时看护运行框架(低开销监控修改的影响:修改正确的触发,对前中后的时间影响,e.g.,cpprinter,CI)
- 批量实验设计:组内原始数据,组件CSV数据对比,整体可视化感知。
- 理解已有设计:只有充分理解设计,才不会有疏忽漏洞和BUG
- 项目拆分,确定看护代码规模
- 先使用彻底理解工具来Profile理解
- 向前兼容设计/重构:
- 最小化修改原则下,回答5W1H
-
实时输出《详细设计文档》
- 小步开发交付:
- 小步敏捷开发。不超过100行代码提交一次PR。
- DEBUG/DEV同时开发,DEV虽然支持打印debug信息,但是没有
-g
的堆栈信息,不能快速GDB。 - 及时保存各种情况的编译包。
- DEBUG(需要真实的证据,和严谨的推导)
- 在同一套添加了DEBUG信息和开启了DEBUG模式的代码里,用环境变量字串,来控制代码中的元修改,通过排列组合的快速测试,来定位到相对于原代码,哪几处的元修改导致了seg fault.
- (不敏捷)使用无限画布,来推导
- 从已知到目标,包括猜想、实验、测试、阶段结论的循环。
- 测试并可视化:
- 由于共用机器,OS等环境很容易发生变化,测试时一定要成组测试,baseline和修改后要一起测试。
- 如果出现BUG,一定是看护规模不够大,影响了外部组件,返回第三步针对debug涉及组件扩大看护规模,重新收集。
-
实时输出《自验文档》,基于哪个gitid的包,进行了哪些对比实验。
-
wiki(使用说明)
- 接口部署
- 回顾分析:提取重点和案例
技巧¶
- 用户需求分类和排序:KANO三层模型
- 根因分析:5WHY,通过连续的询问来明确根本原因。
- 5W+3H: Why,What,Who,When,Where + How/How long/How much
- Why是关注项目的背景:一般是性能瓶颈,不支持流行的功能
- What是具体做什么,实现什么
- How much是这里一般指人力开销(每人天)
- How long是功能的开销
实际情况¶
艰难的开发环境:多人共用Root,可用时间极短
- 任务抢占:在跑高性能任务时,还有其他的人跑
- 任务中断:任务一半被kill,机器一天重启三次
- 环境变化:OS更换内核,g++消失/变版本
权限与隔离
- 文件隔离: 更换
export HOME=/home/t00906153
- shell隔离: zsh shell
由于机器(显卡)可用时间极短,所以要保证测试的有效性,功能开发时要提前单元测试,保证子模块的正确性和测试效率。
庞大的复杂项目:编译慢,测试慢,报错多
torch-npu基于meta的PyTorch,还安装了许多三方库。
需要功能拆分,拆分出独立的功能单元,在其余仓库进行开发、编译和单元测试。之后在快速合并。
庞大的复杂项目:黑盒部分多
几十万行的代码肯定的不懂的地方比理解的地方多。
米山:各自开发,标准不一,重复冗余
每个人都按照自己的功能需求在某个目录下拓展代码,不关心旁边文件夹的内容的实现,导致有重复代码和,不同的定义区别。
需要wiki
方法论¶
- 从已知推进到未知:基于事实论据-> 假设实践(设计编码)-> 测试验证 -> 支持/修正假设。
- 从复杂划分出简单:将复杂项目和需求拆分,缓慢的编译和测试打散,小步开发递进
三层类设计¶
- DAO(数据持久化)层:最底层是聚类数据和基于数据的原子操作,包括保存外部的config或者选项的参数的类config_data。
- Server层:上面一层是根据底层状态数据和外部参数决定的操作组合(工厂类),利用掌握的所有数据类及其上面的元操作进行复杂功能实现。满足开闭原则,这一层添加新函数。也可以再分成两层:偏原子操作类,偏调度类
- Interface/Controller层:在最上一层是外部处理接口, 并且掌握所有的数据类的unique_ptr,和所有的工厂类的使用。作用是建立/初始化工厂类和各数据类直接的关联
Interface / Implementation
上层接口类与下层实现类(面向不同的场景多态)解耦
目标¶
- 期望:延续软件的易拓展性
- 认知的四层境界:不知道自己不知道;知道自己不知道,知道自己知道,不知道自己知道(化繁为简,融为一体,浑然一体)
- 认知的多个阶段:愚昧山峰->自信低谷->持续进步。
- 多用cpp-reference
代码设计¶
四层思想¶
- 目标:易读、易维护的Clean Code。
- 从抽象到具体有如下四层:
聚类平衡:高内聚,低耦合
正交设计四原则
- 消除重复
- 隔离变化
- 缩小依赖范围
- 向稳定依赖
SOLID原则
- Single Responsibility Principle
- Open-Closed Principle:对拓展开放,(已有类/函数)修改关闭
- Liskov Substitution 里氏替换,子类一定可以出现。 LSP是继承复用的基石,只有当衍生类可以替换掉基类,
- Interface Segregation 接口隔离,组合继承(通过继承多个基类
- Dependency Inversion 依赖倒置。实例依赖抽象
22种设计模式
有些常用的典型模式:
- 策略模式 Strategy,一个工厂类包全部
- 门面模式 Facade,为多子类统一对外接口
- 模板模式,抽取多个类的公共部分为基类部分来继承/重载
- 观察者模式 Observe,重点在于如何设计观察的内容(msg的数据量大小)
大致分为四类:
- 创造型
- 行为型
- 结构型
- 告警->观察者模式。
其余原则¶
- 首位是正确性(通过所有测试)
- DRY (Don’t Repeat Yourself)
- KISS (Keep It Simple, Stupid)
- YAGNI (You Ain’t Gonna Need It):不必预期编码,而是预留接口
- SMaRT,安全,可维护,可移植,可测试
- 最小职责(别和陌生人说话
具体实现¶
- BFS写法+分治法
- 函数设计要求
- 多思考函数的必要性,通用性
- 任务、函数解耦; 降低圈复杂度是提升可读性的有效方法。
- 命名简洁有特色,自注释,见字知意,
- 利用参数的信息来简化函数名,比如split(string A, char B)比splitString好。
- 如没必要,尽量简化传参
- 直接返回值,避免在外面初始化重要变量,然后传地址的方式
- 重要的输入,会喜欢写在外面,如果只在函数内使用,这是没必要的
- 代码就近原则,相关功能的代码写一起
- 多用空格
- 高效读代码:BFS读代码方式,边读代码边输出UML类图,
使用上下文图来分析函数的子功能,UML来画类图
- 上下文图工具
- PlantUML vscode 插件
闭环¶
计划、总结反思
- PDCA 戴明环:plan do check act
- ORID :Objective客观 Reflective反应 Interpretive诠释 Decisional决定
命名¶
编程时函数命名和变量命名一直是比较头疼的事情,
函数命名¶
- 经典的动宾短语?
变量命名¶
- 临时变量
- 私有变量:
time_
- 全局变量
typedef
#define
- struct 和 class 命名
代码测试设计¶
- 设计的案例需要覆盖所有独立变量/状态/功能的可能情况,但是不要测量最小独立变量间的排列组合,这不满足正交性。
- 反向验证:如果测试case报错,但是不能一眼定位代码问题,说明测试case不满足正交性,需要拆开。
- 如果建立的完备正交的独立测试集,但是还有代码没有覆盖,说明代码有冗余。
- 好的测试相当于代码文档。
- C++ 可以使用 googletest,可参考相关学习文档。
测试设计虽然一般通过代码覆盖率来评估
如果是先写代码再写测试,很容易跳过简单实现的测试(比如状态的初始化)
测试的分类¶
- UT(Unit Test)
- 多使用
Test Double
,用测试替身来替代测试环境中依赖的外部组件
- 多使用
- IT(Integrated Test)集成测试
- ST(System Test)
非功能测试(比如性能调优代码)单元测试是没有能力覆盖的,
这时可以使用其余的评估指标,e.g, google-benchmark
单元测试设计需要保证正交性,和完备性
- 所有代码覆盖(执行)到,不必须所有case穷尽完。
- 零,负数,int越界值
- 避免没有注释/意义的 魔鬼数字。
测试景深,关注正交的单元功能测试
测试设计原则¶
FIRST原则
什么是好的测试。
- Fast(<1s),
- Isolated独立,
- Repeatable,
- Self-Validating(确定是pass/fail,不要输出log),
- Timely及时测试(TDD)
Right-BICEP原则
- Right、
- Boundary、
- Inverse(反向)、
- Cross-Checking(交叉检查)
- Error、
- Performance
测试设计应该时刻铭记墨菲定律
墨菲定律,又译为摩菲定律,原句是:如果有多种方式去做某事,而其中一种方式将导致灾难,则必定有人会这样选择。在科学和算法方面与英文所谓的“worst-case scenario”
CORRECT原则,主要面向集成测试
- Conformance Consistent 一致性
- Ordering 顺序
- Range 范围
- Existence 存在
- Cardinality 下标,基数越界
- Time
TDD思想¶
- 极限编程核心编程循环:TDD(test-driven development) + 重构 + 简单设计
OOP思想¶
- 面向对象编程:将数据与操作封装在一起,相对于面向过程,虽然略微复杂,但是代码能高内聚,低耦合,提高了程序后续的可拓展性
- 面向对象的软件设计原则: 开闭原则、单一职责、依赖倒置、里氏替换
- 通过封装(访问控制)、继承(复用父类函数)、多态(重载函数实现)来实习
代码重构¶
- 定义:保持外部接口的时候,简化代码
- 目的:提高开发效率,使得代码易于理解与易于拓展
- 何时重构:有时间时、对核心和经常WTF的代码来重构
- 如何重构:小步提交。
- 识别坏味道的多种方法
- 抽取、 替换、 组成、 改名、 移动。