跳转至

Programming Principle & Code Test

导言

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

实际情况

艰难的开发环境:多人共用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,和所有的工厂类的使用。作用是建立/初始化工厂类和各数据类直接的关联

目标

  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决定

代码测试设计

  • 设计的案例需要覆盖所有独立变量/状态/功能的可能情况,但是不要测量最小独立变量间的排列组合,这不满足正交性。
  • 反向验证:如果测试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的代码来重构
  • 如何重构:小步提交。
    • 识别坏味道的多种方法
    • 抽取、 替换、 组成、 改名、 移动。

参考文献

评论