跳转至

Programming Principle & Code Test

导言

大型团队项目代码的编写,有些要求和原则需要遵守。

项目闭环

流程

  1. 需求澄清明确:理解背景、功能/收益点、基本设计与实现思路。
    1. 人无我有,人有我优,人优我异。
    2. 竞争力 = 质量/成本/进度 三角;保证质量优先。
  2. 开发框架构建:
    1. 敏捷开发流程(快速编译涉及的小单元、快速测试小例子)
    2. 彻底理解工具(或许耗时,但有方法能彻底理解逻辑,e.g.,pytorch.profile,viztracer,GDB)
    3. 实时看护运行框架(低开销监控修改的影响:修改正确的触发,对前中后的时间影响,e.g.,cpprinter,CI)
    4. 批量实验设计:组内原始数据,组件CSV数据对比,整体可视化感知。
  3. 理解已有设计:只有充分理解设计,才不会有疏忽漏洞和BUG
    1. 项目拆分,确定看护代码规模
    2. 先使用彻底理解工具来Profile理解
  4. 向前兼容设计/重构:
    1. 最小化修改原则下,回答5W1H
    2. 实时输出《详细设计文档》

  5. 小步开发交付:
    1. 小步敏捷开发。不超过100行代码提交一次PR。
    2. DEBUG/DEV同时开发,DEV虽然支持打印debug信息,但是没有-g的堆栈信息,不能快速GDB。
    3. 及时保存各种情况的编译包。
  6. DEBUG(需要真实的证据,和严谨的推导)
    1. 在同一套添加了DEBUG信息和开启了DEBUG模式的代码里,用环境变量字串,来控制代码中的元修改,通过排列组合的快速测试,来定位到相对于原代码,哪几处的元修改导致了seg fault.
    2. (不敏捷)使用无限画布,来推导
    3. 从已知到目标,包括猜想、实验、测试、阶段结论的循环。
  7. 测试并可视化:
    1. 由于共用机器,OS等环境很容易发生变化,测试时一定要成组测试,baseline和修改后要一起测试。
    2. 如果出现BUG,一定是看护规模不够大,影响了外部组件,返回第三步针对debug涉及组件扩大看护规模,重新收集。
    3. 实时输出《自验文档》,基于哪个gitid的包,进行了哪些对比实验。

    4. wiki(使用说明)

  8. 接口部署
  9. 回顾分析:提取重点和案例

技巧

  1. 用户需求分类和排序:KANO三层模型
  2. 根因分析:5WHY,通过连续的询问来明确根本原因。
  3. 5W+3H: Why,What,Who,When,Where + How/How long/How much
    1. Why是关注项目的背景:一般是性能瓶颈,不支持流行的功能
    2. What是具体做什么,实现什么
    3. How much是这里一般指人力开销(每人天)
    4. How long是功能的开销

实际情况

艰难的开发环境:多人共用Root,可用时间极短
  1. 任务抢占:在跑高性能任务时,还有其他的人跑
  2. 任务中断:任务一半被kill,机器一天重启三次
  3. 环境变化:OS更换内核,g++消失/变版本
权限与隔离
  1. 文件隔离: 更换export HOME=/home/t00906153
  2. shell隔离: zsh shell

由于机器(显卡)可用时间极短,所以要保证测试的有效性,功能开发时要提前单元测试,保证子模块的正确性测试效率

庞大的复杂项目:编译慢,测试慢,报错多

torch-npu基于meta的PyTorch,还安装了许多三方库。

需要功能拆分,拆分出独立的功能单元,在其余仓库进行开发、编译和单元测试。之后在快速合并。

庞大的复杂项目:黑盒部分多

几十万行的代码肯定的不懂的地方比理解的地方多。

米山:各自开发,标准不一,重复冗余

每个人都按照自己的功能需求在某个目录下拓展代码,不关心旁边文件夹的内容的实现,导致有重复代码和,不同的定义区别。

需要wiki

方法论

  1. 从已知推进到未知:基于事实论据-> 假设实践(设计编码)-> 测试验证 -> 支持/修正假设。
  2. 从复杂划分出简单:将复杂项目和需求拆分,缓慢的编译和测试打散,小步开发递进

三层类设计

  1. DAO(数据持久化)层:最底层是聚类数据和基于数据的原子操作,包括保存外部的config或者选项的参数的类config_data。
  2. Server层:上面一层是根据底层状态数据和外部参数决定的操作组合(工厂类),利用掌握的所有数据类及其上面的元操作进行复杂功能实现。满足开闭原则,这一层添加新函数。也可以再分成两层:偏原子操作类,偏调度类
  3. Interface/Controller层:在最上一层是外部处理接口, 并且掌握所有的数据类的unique_ptr,和所有的工厂类的使用。作用是建立/初始化工厂类和各数据类直接的关联

Interface / Implementation

上层接口类与下层实现类(面向不同的场景多态)解耦

目标

  1. 期望:延续软件的易拓展性
  2. 认知的四层境界:不知道自己不知道;知道自己不知道,知道自己知道,不知道自己知道(化繁为简,融为一体,浑然一体)
  3. 认知的多个阶段:愚昧山峰->自信低谷->持续进步。
  4. 多用cpp-reference

代码设计

四层思想

  • 目标:易读、易维护的Clean Code。
  • 从抽象到具体有如下四层:
聚类平衡:高内聚,低耦合
正交设计四原则
  1. 消除重复
  2. 隔离变化
  3. 缩小依赖范围
  4. 向稳定依赖
SOLID原则
  1. Single Responsibility Principle
  2. Open-Closed Principle:对拓展开放,(已有类/函数)修改关闭
  3. Liskov Substitution 里氏替换,子类一定可以出现。 LSP是继承复用的基石,只有当衍生类可以替换掉基类,
  4. Interface Segregation 接口隔离,组合继承(通过继承多个基类
  5. Dependency Inversion 依赖倒置。实例依赖抽象
22种设计模式

具体参考2,参考书:大话设计模式

有些常用的典型模式

  1. 策略模式 Strategy,一个工厂类包全部
  2. 门面模式 Facade,为多子类统一对外接口
  3. 模板模式,抽取多个类的公共部分为基类部分来继承/重载
  4. 观察者模式 Observe,重点在于如何设计观察的内容(msg的数据量大小)

大致分为四类:

  1. 创造型
  2. 行为型
  3. 结构型
  4. 告警->观察者模式。

其余原则

  1. 首位是正确性(通过所有测试)
  2. DRY (Don’t Repeat Yourself)
  3. KISS (Keep It Simple, Stupid)
  4. YAGNI (You Ain’t Gonna Need It):不必预期编码,而是预留接口
  5. SMaRT,安全,可维护,可移植,可测试
  6. 最小职责(别和陌生人说话

具体实现

  1. BFS写法+分治法
  2. 函数设计要求
    1. 多思考函数的必要性,通用性
    2. 任务、函数解耦; 降低圈复杂度是提升可读性的有效方法。
    3. 命名简洁有特色,自注释,见字知意
    4. 利用参数的信息来简化函数名,比如split(string A, char B)比splitString好。
  3. 如没必要,尽量简化传参
    1. 直接返回值,避免在外面初始化重要变量,然后传地址的方式
    2. 重要的输入,会喜欢写在外面,如果只在函数内使用,这是没必要的
  4. 代码就近原则,相关功能的代码写一起
  5. 多用空格
  6. 高效读代码:BFS读代码方式,边读代码边输出UML类图,

使用上下文图来分析函数的子功能,UML来画类图

闭环

计划、总结反思

  • PDCA 戴明环:plan do check act
  • ORID :Objective客观 Reflective反应 Interpretive诠释 Decisional决定

命名

编程时函数命名和变量命名一直是比较头疼的事情,

函数命名

  1. 经典的动宾短语?

变量命名

  1. 临时变量
  2. 私有变量: time_
  3. 全局变量
  4. typedef
  5. #define
  6. struct 和 class 命名

代码测试设计

  • 设计的案例需要覆盖所有独立变量/状态/功能的可能情况,但是不要测量最小独立变量间的排列组合,这不满足正交性。
  • 反向验证:如果测试case报错,但是不能一眼定位代码问题,说明测试case不满足正交性,需要拆开。
  • 如果建立的完备正交的独立测试集,但是还有代码没有覆盖,说明代码有冗余。
  • 好的测试相当于代码文档。
  • C++ 可以使用 googletest,可参考相关学习文档

测试设计虽然一般通过代码覆盖率来评估

如果是先写代码再写测试,很容易跳过简单实现的测试(比如状态的初始化)

测试的分类

  • UT(Unit Test)
    • 多使用Test Double,用测试替身来替代测试环境中依赖的外部组件
  • IT(Integrated Test)集成测试
  • ST(System Test)

非功能测试(比如性能调优代码)单元测试是没有能力覆盖的,

这时可以使用其余的评估指标,e.g, google-benchmark

单元测试设计需要保证正交性,和完备性

  1. 所有代码覆盖(执行)到,不必须所有case穷尽完。
  2. 零,负数,int越界值
  3. 避免没有注释/意义的 魔鬼数字。

测试景深,关注正交的单元功能测试

测试设计原则

FIRST原则

什么是好的测试。

  1. Fast(<1s),
  2. Isolated独立,
  3. Repeatable,
  4. Self-Validating(确定是pass/fail,不要输出log),
  5. Timely及时测试(TDD)
Right-BICEP原则
  1. Right、
  2. Boundary、
  3. Inverse(反向)、
  4. Cross-Checking(交叉检查)
  5. Error、
  6. Performance

测试设计应该时刻铭记墨菲定律

墨菲定律,又译为摩菲定律,原句是:如果有多种方式去做某事,而其中一种方式将导致灾难,则必定有人会这样选择。在科学和算法方面与英文所谓的“worst-case scenario”

CORRECT原则,主要面向集成测试
  1. Conformance Consistent 一致性
  2. Ordering 顺序
  3. Range 范围
  4. Existence 存在
  5. Cardinality 下标,基数越界
  6. Time

TDD思想

  • 极限编程核心编程循环:TDD(test-driven development) + 重构 + 简单设计

OOP思想

  • 面向对象编程:将数据与操作封装在一起,相对于面向过程,虽然略微复杂,但是代码能高内聚,低耦合,提高了程序后续的可拓展性
    • 面向对象的软件设计原则: 开闭原则、单一职责、依赖倒置、里氏替换
  • 通过封装(访问控制)、继承(复用父类函数)、多态(重载函数实现)来实习

代码重构

  • 定义:保持外部接口的时候,简化代码
  • 目的:提高开发效率,使得代码易于理解与易于拓展
  • 何时重构:有时间时、对核心和经常WTF的代码来重构
  • 如何重构:小步提交。
    • 识别坏味道的多种方法
    • 抽取、 替换、 组成、 改名、 移动。

参考文献

评论